Skip to content

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

  1. Architecture Overview
  2. Authentication
  3. API Client Setup
  4. Data Models & Structures
  5. Common Patterns
  6. Error Handling
  7. UI/UX Patterns
  8. 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:

javascript
const API_BASE_URL = 'https://your-company.erpax.com';
// or for development: 'http://localhost:3000'

Authentication

For browser-based applications, use session cookies:

javascript
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:

javascript
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:

javascript
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

javascript
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:

javascript
// 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):

javascript
{
  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:

javascript
{
  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:

javascript
{
  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

javascript
// 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

javascript
// 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

javascript
// Update invoice
async updateInvoice(invoiceId, updates) {
  return this.patch(`/admin/invoices/${invoiceId}.json`, {
    invoice: updates
  });
}

Deleting Resources

javascript
// Delete invoice
async deleteInvoice(invoiceId) {
  return this.delete(`/admin/invoices/${invoiceId}.json`);
}

Batch Operations

javascript
// 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

javascript
{
  error: "Error message",
  errors: {
    field_name: ["Error message 1", "Error message 2"]
  }
}

Error Handling Implementation

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
<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

javascript
// 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:

Next Steps

  1. Review Getting Started for authentication setup
  2. Explore Platform Overview for architecture details
  3. Check Complete API Reference for query syntax, validation rules, and business rules
  4. Check API Routes for all available endpoints (each route includes relationship information)
  5. Review Features for available capabilities

All information needed to build a detached frontend is contained in this documentation. No backend code access is required.

Released under an open source license.