Frontend Development Guide
This guide provides everything you need to build a completely detached frontend application that integrates with ERPax. All information required for frontend development is contained in this documentation.
Table of Contents
- Architecture Overview
- Authentication
- API Client Setup
- Data Models & Structures
- Common Patterns
- Error Handling
- UI/UX Patterns
- Complete Examples
Architecture Overview
Detached Frontend Architecture
ERPax is designed for detached frontend applications. The frontend communicates with ERPax exclusively through the RESTful JSON API.
┌─────────────────┐
│ Frontend App │
│ (React/Vue/ │
│ Angular/etc) │
└────────┬────────┘
│ HTTP/HTTPS
│ JSON API
▼
┌─────────────────┐
│ ERPax API │
│ (SaaS Backend) │
└─────────────────┘Key Principles:
- Frontend is completely independent
- All communication via RESTful JSON API
- No backend code in frontend
- Session or token-based authentication
- Multi-tenant via domain/hostname
API Base URL
All API requests use your ERPax instance URL:
const API_BASE_URL = 'https://your-company.erpax.com';
// or for development: 'http://localhost:3000'Authentication
Session-Based Authentication (Recommended for Browser Apps)
For browser-based applications, use session cookies:
class ERPaxAuth {
constructor(baseURL) {
this.baseURL = baseURL;
}
async login(email, password) {
const response = await fetch(`${this.baseURL}/users/sign_in.json`, {
method: 'POST',
credentials: 'include', // Critical: include cookies
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
user: {
email: email,
password: password
}
})
});
if (response.ok) {
const data = await response.json();
return { success: true, user: data.user };
} else {
const error = await response.json();
return { success: false, error: error };
}
}
async logout() {
const response = await fetch(`${this.baseURL}/users/sign_out.json`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
return response.ok;
}
async getCurrentUser() {
const response = await fetch(`${this.baseURL}/admin/users/current.json`, {
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
return await response.json();
}
return null;
}
}Token-Based Authentication (For API Integrations)
For server-to-server or mobile apps:
class ERPaxTokenAuth {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = null;
}
async login(email, password) {
const response = await fetch(`${this.baseURL}/api/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password
})
});
if (response.ok) {
const data = await response.json();
this.token = data.token;
return { success: true, token: data.token };
} else {
return { success: false, error: await response.json() };
}
}
getAuthHeaders() {
return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
}CSRF Token (For Session-Based Auth)
When making POST/PATCH/DELETE requests with session auth, include CSRF token:
function getCSRFToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.content : null;
}
// Use in requests
headers: {
'X-CSRF-Token': getCSRFToken(),
'Content-Type': 'application/json'
}API Client Setup
Complete API Client Implementation
class ERPaxClient {
constructor(baseURL, authMethod = 'session') {
this.baseURL = baseURL;
this.authMethod = authMethod;
this.token = null;
}
// Get request headers
getHeaders(includeCSRF = false) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (this.authMethod === 'token' && this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
if (includeCSRF) {
const csrfToken = this.getCSRFToken();
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
}
}
return headers;
}
getCSRFToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.content : null;
}
// Make authenticated request
async request(method, path, data = null, options = {}) {
const url = `${this.baseURL}${path}`;
const config = {
method: method,
credentials: this.authMethod === 'session' ? 'include' : 'omit',
headers: this.getHeaders(['POST', 'PATCH', 'PUT', 'DELETE'].includes(method))
};
if (data) {
config.body = JSON.stringify(data);
}
const response = await fetch(url, { ...config, ...options });
if (!response.ok) {
const error = await response.json().catch(() => ({
error: response.statusText
}));
throw new ERPaxError(response.status, error);
}
return await response.json();
}
// Convenience methods
async get(path, options = {}) {
return this.request('GET', path, null, options);
}
async post(path, data, options = {}) {
return this.request('POST', path, data, options);
}
async patch(path, data, options = {}) {
return this.request('PATCH', path, data, options);
}
async delete(path, options = {}) {
return this.request('DELETE', path, null, options);
}
}
// Custom error class
class ERPaxError extends Error {
constructor(status, data) {
super(data.error || `API Error: ${status}`);
this.status = status;
this.data = data;
}
}Data Models & Structures
Relationship information is documented in each route's documentation. See Invoices, Addresses, Items, etc.
Invoice Model
The Invoice model is the unified document model for all business documents:
// Invoice structure
{
id: 1,
number: "INV-2025-001",
invoice_type: "invoice", // or "quotation", "purchase_order", etc.
date: "2025-01-15",
due_date: "2025-02-15",
seller_id: 1,
buyer_id: 2,
currency_code: "USD",
total_amount_cents: 100000,
formatted_amount: "$1,000.00",
status: "confirmed",
invoice_lines: [
{
id: 1,
seller_item_id: 10,
description: "Product Name",
quantity: 5,
unit_price_cents: 10000,
total_amount_cents: 50000,
formatted_amount: "$500.00"
}
],
seller: {
id: 1,
name: "Seller Company",
email: "[email protected]"
},
buyer: {
id: 2,
name: "Buyer Company",
email: "[email protected]"
}
}Address Model
Unified model for all business partners (customers, suppliers, sellers, buyers):
{
id: 1,
code: "CUST-001",
name: "Customer Name",
email: "[email protected]",
phone: "+1234567890",
currency_code: "USD",
address_type: "customer", // or "supplier", "seller", "buyer"
address_lines: [
"123 Main St",
"City, State 12345"
]
}Invoice Line Model
Line items within an invoice:
{
id: 1,
invoice_id: 1,
seller_item_id: 10,
description: "Product Description",
quantity: 5,
unit_price_cents: 10000,
total_amount_cents: 50000,
formatted_amount: "$500.00",
item: {
id: 10,
code: "PROD-001",
name: "Product Name"
}
}Payment Model
Payment records:
{
id: 1,
invoice_id: 1,
amount_cents: 100000,
formatted_amount: "$1,000.00",
payment_date: "2025-01-20",
payment_method: "bank_transfer",
reference: "REF-12345"
}Common Patterns
Listing Resources with Filters
// List invoices with filters
async listInvoices(filters = {}) {
const params = new URLSearchParams();
// Ransack query syntax
Object.entries(filters).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
params.append(`q[${key}]`, value);
}
});
return this.get(`/admin/invoices.json?${params}`);
}
// Usage
const invoices = await client.listInvoices({
invoice_type_eq: 'invoice',
status_eq: 'confirmed',
date_gteq: '2025-01-01',
date_lteq: '2025-12-31'
});Creating Resources
// Create invoice
async createInvoice(invoiceData) {
return this.post('/admin/invoices.json', {
invoice: {
invoice_type: invoiceData.type,
seller_id: invoiceData.sellerId,
buyer_id: invoiceData.buyerId,
date: invoiceData.date,
currency_code: invoiceData.currency,
invoice_lines_attributes: invoiceData.lines.map(line => ({
seller_item_id: line.itemId,
quantity: line.quantity,
unit_price_cents: line.unitPriceCents
}))
}
});
}Updating Resources
// Update invoice
async updateInvoice(invoiceId, updates) {
return this.patch(`/admin/invoices/${invoiceId}.json`, {
invoice: updates
});
}Deleting Resources
// Delete invoice
async deleteInvoice(invoiceId) {
return this.delete(`/admin/invoices/${invoiceId}.json`);
}Batch Operations
// Batch action (e.g., batch delete)
async batchAction(resource, action, ids) {
return this.post(`/admin/${resource}/batch_action`, {
batch_action: action,
collection_selection: ids
});
}Error Handling
Standard Error Response Format
{
error: "Error message",
errors: {
field_name: ["Error message 1", "Error message 2"]
}
}Error Handling Implementation
class ERPaxErrorHandler {
static handle(error) {
if (error instanceof ERPaxError) {
switch (error.status) {
case 401:
// Unauthorized - redirect to login
window.location.href = '/login';
break;
case 403:
// Forbidden - show permission error
this.showError('You do not have permission to perform this action');
break;
case 422:
// Validation errors
this.showValidationErrors(error.data.errors);
break;
case 500:
// Server error
this.showError('Server error. Please try again later.');
break;
default:
this.showError(error.message);
}
} else {
this.showError('An unexpected error occurred');
}
}
static showValidationErrors(errors) {
Object.entries(errors).forEach(([field, messages]) => {
const fieldElement = document.querySelector(`[name="${field}"]`);
if (fieldElement) {
fieldElement.classList.add('error');
const errorMsg = document.createElement('div');
errorMsg.className = 'error-message';
errorMsg.textContent = messages.join(', ');
fieldElement.parentNode.appendChild(errorMsg);
}
});
}
static showError(message) {
// Implement your error display logic
console.error(message);
}
}UI/UX Patterns
Loading States
class LoadingState {
constructor(element) {
this.element = element;
this.isLoading = false;
}
start() {
this.isLoading = true;
this.element.classList.add('loading');
this.element.setAttribute('disabled', 'disabled');
}
stop() {
this.isLoading = false;
this.element.classList.remove('loading');
this.element.removeAttribute('disabled');
}
}Form Handling
class InvoiceForm {
constructor(formElement, client) {
this.form = formElement;
this.client = client;
this.setupForm();
}
setupForm() {
this.form.addEventListener('submit', async (e) => {
e.preventDefault();
await this.submit();
});
}
async submit() {
const formData = new FormData(this.form);
const invoiceData = {
invoice_type: formData.get('invoice_type'),
buyer_id: parseInt(formData.get('buyer_id')),
date: formData.get('date'),
currency_code: formData.get('currency_code'),
invoice_lines_attributes: this.getInvoiceLines()
};
try {
const result = await this.client.createInvoice(invoiceData);
this.onSuccess(result);
} catch (error) {
ERPaxErrorHandler.handle(error);
}
}
getInvoiceLines() {
// Extract line items from form
const lines = [];
const lineElements = this.form.querySelectorAll('.invoice-line');
lineElements.forEach(lineEl => {
lines.push({
seller_item_id: parseInt(lineEl.querySelector('[name="item_id"]').value),
quantity: parseFloat(lineEl.querySelector('[name="quantity"]').value),
unit_price_cents: parseInt(lineEl.querySelector('[name="unit_price"]').value) * 100
});
});
return lines;
}
onSuccess(result) {
// Handle success (redirect, show message, etc.)
window.location.href = `/invoices/${result.invoice.id}`;
}
}Data Table with Pagination
class DataTable {
constructor(container, client, resource) {
this.container = container;
this.client = client;
this.resource = resource;
this.currentPage = 1;
this.perPage = 20;
this.filters = {};
}
async load() {
const params = new URLSearchParams({
page: this.currentPage,
per_page: this.perPage,
...this.buildFilters()
});
const data = await this.client.get(`/admin/${this.resource}.json?${params}`);
this.render(data);
}
buildFilters() {
const filters = {};
Object.entries(this.filters).forEach(([key, value]) => {
if (value) {
filters[`q[${key}]`] = value;
}
});
return filters;
}
render(data) {
const table = this.createTable(data);
this.container.innerHTML = '';
this.container.appendChild(table);
this.renderPagination(data);
}
createTable(data) {
const table = document.createElement('table');
// Build table from data
return table;
}
renderPagination(data) {
// Build pagination controls
}
}Complete Examples
React Example
import React, { useState, useEffect } from 'react';
import { ERPaxClient } from './erpax-client';
function InvoiceList() {
const [invoices, setInvoices] = useState([]);
const [loading, setLoading] = useState(true);
const client = new ERPaxClient(process.env.REACT_APP_API_URL);
useEffect(() => {
loadInvoices();
}, []);
async function loadInvoices() {
try {
setLoading(true);
const data = await client.get('/admin/invoices.json');
setInvoices(data.invoices || []);
} catch (error) {
console.error('Error loading invoices:', error);
} finally {
setLoading(false);
}
}
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>Invoices</h1>
<table>
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Buyer</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{invoices.map(invoice => (
<tr key={invoice.id}>
<td>{invoice.number}</td>
<td>{invoice.date}</td>
<td>{invoice.buyer?.name}</td>
<td>{invoice.formatted_amount}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default InvoiceList;Vue.js Example
<template>
<div>
<h1>Invoices</h1>
<table v-if="!loading">
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Buyer</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr v-for="invoice in invoices" :key="invoice.id">
<td>{{ invoice.number }}</td>
<td>{{ invoice.date }}</td>
<td>{{ invoice.buyer?.name }}</td>
<td>{{ invoice.formatted_amount }}</td>
</tr>
</tbody>
</table>
<div v-else>Loading...</div>
</div>
</template>
<script>
import { ERPaxClient } from './erpax-client';
export default {
data() {
return {
invoices: [],
loading: true,
client: new ERPaxClient(process.env.VUE_APP_API_URL)
};
},
async mounted() {
await this.loadInvoices();
},
methods: {
async loadInvoices() {
try {
this.loading = true;
const data = await this.client.get('/admin/invoices.json');
this.invoices = data.invoices || [];
} catch (error) {
console.error('Error loading invoices:', error);
} finally {
this.loading = false;
}
}
}
};
</script>Vanilla JavaScript Example
// Complete working example
class InvoiceApp {
constructor() {
this.client = new ERPaxClient('https://your-company.erpax.com');
this.init();
}
async init() {
await this.setupAuth();
await this.loadInvoices();
}
async setupAuth() {
// Check if already authenticated
const user = await this.client.getCurrentUser();
if (!user) {
// Redirect to login
window.location.href = '/login.html';
return;
}
}
async loadInvoices() {
try {
const data = await this.client.get('/admin/invoices.json');
this.renderInvoices(data.invoices || []);
} catch (error) {
ERPaxErrorHandler.handle(error);
}
}
renderInvoices(invoices) {
const container = document.getElementById('invoices');
container.innerHTML = invoices.map(invoice => `
<div class="invoice-card">
<h3>${invoice.number}</h3>
<p>Date: ${invoice.date}</p>
<p>Buyer: ${invoice.buyer?.name}</p>
<p>Amount: ${invoice.formatted_amount}</p>
</div>
`).join('');
}
}
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
new InvoiceApp();
});API Reference
For complete API documentation, see:
- Admin API - Admin namespace endpoints
- Sales API - Sales namespace endpoints
- Client API - Client namespace endpoints
- System API - System namespace endpoints
Next Steps
- Review Getting Started for authentication setup
- Explore Platform Overview for architecture details
- Check Complete API Reference for query syntax, validation rules, and business rules
- Check API Routes for all available endpoints (each route includes relationship information)
- Review Features for available capabilities
All information needed to build a detached frontend is contained in this documentation. No backend code access is required.