Skip to content

Frontend Examples

Complete, working examples of detached frontend applications that integrate with ERPax using only the API. All examples follow patterns from the Frontend Development Guide.

Vanilla JavaScript Example

A minimal, self-contained example using vanilla JavaScript. Perfect for understanding the basics.

Features

  • ✅ Session-based authentication
  • ✅ List invoices with filtering
  • ✅ Create invoices
  • ✅ View invoice details
  • ✅ Delete invoices
  • ✅ Error handling
  • ✅ Loading states

Complete Implementation

Configuration

javascript:examples/vanilla-js/config.js
// ERPax API Configuration
const CONFIG = {
  // Update this to your ERPax instance URL
  API_BASE_URL: 'https://your-company.erpax.com',
  // For local development: 'http://localhost:3000'
  
  // API endpoints
  ENDPOINTS: {
    LOGIN: '/users/sign_in.json',
    LOGOUT: '/users/sign_out.json',
    CURRENT_USER: '/admin/users/current.json',
    INVOICES: '/admin/invoices.json',
    ADDRESSES: '/admin/addresses.json',
    ITEMS: '/admin/items.json'
  }
};

API Client

javascript:examples/vanilla-js/erpax-client.js
/**
 * ERPax API Client
 * Complete implementation following /docs/frontend-development.md
 */
class ERPaxClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  getHeaders(includeCSRF = false) {
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    };
    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;
  }
  async request(method, path, data = null, options = {}) {
    const url = `${this.baseURL}${path}`;
    const config = {
      method: method,
      credentials: 'include', // Important: include cookies for session auth
      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);
  }
  // Authentication methods
  async login(email, password) {
    return this.post(CONFIG.ENDPOINTS.LOGIN, {
      user: { email, password }
    });
  }
  async logout() {
    return this.delete(CONFIG.ENDPOINTS.LOGOUT);
  }
  async getCurrentUser() {
    try {
      return await this.get(CONFIG.ENDPOINTS.CURRENT_USER);
    } catch (error) {
      return null;
    }
  }
  // Invoice methods
  async getInvoices(filters = {}) {
    const params = new URLSearchParams();
    Object.entries(filters).forEach(([key, value]) => {
      if (value !== null && value !== undefined) {
        params.append(`q[${key}]`, value);
      }
    });
    const queryString = params.toString();
    const path = queryString ? `${CONFIG.ENDPOINTS.INVOICES}?${queryString}` : CONFIG.ENDPOINTS.INVOICES;
    return this.get(path);
  }
  async getInvoice(id) {
    return this.get(`${CONFIG.ENDPOINTS.INVOICES.replace('.json', '')}/${id}.json`);
  }
  async createInvoice(invoiceData) {
    return this.post(CONFIG.ENDPOINTS.INVOICES, { invoice: invoiceData });
  }
  async updateInvoice(id, updates) {
    return this.patch(`${CONFIG.ENDPOINTS.INVOICES.replace('.json', '')}/${id}.json`, { invoice: updates });
  }
  async deleteInvoice(id) {
    return this.delete(`${CONFIG.ENDPOINTS.INVOICES.replace('.json', '')}/${id}.json`);
  }
}
/**
 * Custom error class for API errors
 */
class ERPaxError extends Error {
  constructor(status, data) {
    super(data.error || `API Error: ${status}`);
    this.status = status;
    this.data = data;
  }
}

Application Logic

javascript:examples/vanilla-js/app.js
/**
 * Main Application
 * Follows patterns from /docs/frontend-development.md
 */
// Initialize API client
const client = new ERPaxClient(CONFIG.API_BASE_URL);
// Application state
let currentUser = null;
let invoices = [];
// Initialize app
document.addEventListener('DOMContentLoaded', async () => {
  await checkAuth();
  setupEventListeners();
});
async function checkAuth() {
  try {
    const response = await client.getCurrentUser();
    if (response && response.user) {
      currentUser = response.user;
      showApp();
      await loadInvoices();
    } else {
      showLogin();
    }
  } catch (error) {
    showLogin();
  }
}
async function loadInvoices(filters = {}) {
  try {
    setLoading(true);
    const response = await client.getInvoices(filters);
    invoices = response.invoices || [];
    renderInvoices();
    clearErrors();
  } catch (error) {
    showError('Failed to load invoices.');
    handleError(error);
  } finally {
    setLoading(false);
  }
}
async function handleLogin(e) {
  e.preventDefault();
  const email = document.getElementById('email').value;
  const password = document.getElementById('password').value;
  
  try {
    setLoading(true);
    await client.login(email, password);
    await checkAuth();
    clearErrors();
  } catch (error) {
    showError('Login failed. Please check your credentials.');
  } finally {
    setLoading(false);
  }
}
async function handleCreateInvoice(e) {
  e.preventDefault();
  
  const formData = new FormData(e.target);
  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') || 'USD',
    invoice_lines_attributes: getInvoiceLines()
  };
  
  try {
    setLoading(true);
    await client.createInvoice(invoiceData);
    e.target.reset();
    await loadInvoices();
    showSuccess('Invoice created successfully!');
  } catch (error) {
    showError('Failed to create invoice.');
    handleError(error);
  } finally {
    setLoading(false);
  }
}
function renderInvoices() {
  const container = document.getElementById('invoices-list');
  
  if (invoices.length === 0) {
    container.innerHTML = '<p>No invoices found.</p>';
    return;
  }
  
  const table = `
    <table class="invoices-table">
      <thead>
        <tr>
          <th>Number</th>
          <th>Type</th>
          <th>Date</th>
          <th>Buyer</th>
          <th>Amount</th>
          <th>Status</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        ${invoices.map(invoice => `
          <tr>
            <td>${invoice.number || 'Draft'}</td>
            <td>${invoice.invoice_type}</td>
            <td>${invoice.date || '-'}</td>
            <td>${invoice.buyer?.name || '-'}</td>
            <td>${invoice.formatted_amount || '$0.00'}</td>
            <td>${invoice.status || 'draft'}</td>
            <td>
              <button onclick="viewInvoice(${invoice.id})">View</button>
              <button onclick="deleteInvoice(${invoice.id})">Delete</button>
            </td>
          </tr>
        `).join('')}
      </tbody>
    </table>
  `;
  
  container.innerHTML = table;
}
function handleError(error) {
  if (error instanceof ERPaxError) {
    if (error.status === 401) {
      showLogin();
    } else if (error.status === 422 && error.data.errors) {
      showValidationErrors(error.data.errors);
    }
  }
}

HTML Structure

html:examples/vanilla-js/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="csrf-token" content="your-csrf-token-here">
  <title>ERPax Frontend Example</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <!-- Login Section -->
    <section id="login-section">
      <div class="card">
        <h2>Login</h2>
        <form id="login-form">
          <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>
          </div>
          <div class="form-group">
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
          </div>
          <button type="submit">Login</button>
        </form>
      </div>
    </section>
    <!-- Main App Section -->
    <section id="app-section">
      <div class="app-header">
        <h2>Invoices</h2>
        <button id="logout-btn">Logout</button>
      </div>
      <!-- Create Invoice Form -->
      <div class="card">
        <h3>Create Invoice</h3>
        <form id="invoice-form">
          <div class="form-row">
            <div class="form-group">
              <label for="invoice_type">Invoice Type:</label>
              <select id="invoice_type" name="invoice_type" required>
                <option value="invoice">Invoice</option>
                <option value="quotation">Quotation</option>
              </select>
            </div>
            <div class="form-group">
              <label for="buyer_id">Buyer ID:</label>
              <input type="number" id="buyer_id" name="buyer_id" required>
            </div>
            <div class="form-group">
              <label for="date">Date:</label>
              <input type="date" id="date" name="date" required>
            </div>
          </div>
          <button type="submit">Create Invoice</button>
        </form>
      </div>
      <!-- Invoices List -->
      <div class="card">
        <h3>Invoices List</h3>
        <div id="invoices-list"></div>
      </div>
    </section>
  </div>
  <script src="config.js"></script>
  <script src="erpax-client.js"></script>
  <script src="app.js"></script>
</body>
</html>

Setup Instructions

  1. Update Configuration Edit config.js and set your ERPax API URL:
    javascript
    const CONFIG = {
      API_BASE_URL: 'https://your-company.erpax.com',
    };
  2. Run the Example
    bash
    # Using Python
    python3 -m http.server 8000
    
    # Using Node.js
    npx http-server -p 8000
    
    # Then visit http://localhost:8000
  3. Usage
    • Login with your ERPax credentials
    • View invoices list
    • Create new invoices
    • View invoice details
    • Delete invoices

Complete Files

All files for this example are available in docs/examples/vanilla-js/:

  • config.js - API configuration
  • erpax-client.js - Complete API client
  • app.js - Application logic
  • index.html - HTML structure
  • styles.css - Styling

React Example

A complete React application with modern patterns.

Features

  • React hooks
  • Context API for state management
  • Form handling
  • Data tables with pagination
  • Error boundaries

Implementation

jsx:examples/react/src/App.jsx
import React, { useState, useEffect } from 'react';
import { ERPaxClient } from './erpax-client';
import InvoiceList from './components/InvoiceList';
import InvoiceForm from './components/InvoiceForm';
import Login from './components/Login';
function App() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const client = new ERPaxClient(process.env.REACT_APP_API_URL);
  useEffect(() => {
    checkAuth();
  }, []);
  async function checkAuth() {
    try {
      const response = await client.getCurrentUser();
      if (response?.user) {
        setUser(response.user);
      }
    } catch (error) {
      console.error('Auth check failed:', error);
    } finally {
      setLoading(false);
    }
  }
  async function handleLogin(email, password) {
    try {
      await client.login(email, password);
      await checkAuth();
    } catch (error) {
      throw error;
    }
  }
  if (loading) return <div>Loading...</div>;
  if (!user) {
    return <Login onLogin={handleLogin} />;
  }
  return (
    <div className="app">
      <header>
        <h1>ERPax Frontend</h1>
        <button onClick={() => client.logout().then(() => setUser(null))}>
          Logout
        </button>
      </header>
      <main>
        <InvoiceForm client={client} />
        <InvoiceList client={client} />
      </main>
    </div>
  );
}
export default App;
jsx:examples/react/src/components/InvoiceList.jsx
import React, { useState, useEffect } from 'react';
function InvoiceList({ client }) {
  const [invoices, setInvoices] = useState([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    loadInvoices();
  }, []);
  async function loadInvoices() {
    try {
      setLoading(true);
      const data = await client.getInvoices();
      setInvoices(data.invoices || []);
    } catch (error) {
      console.error('Error loading invoices:', error);
    } finally {
      setLoading(false);
    }
  }
  if (loading) return <div>Loading invoices...</div>;
  return (
    <div>
      <h2>Invoices</h2>
      <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;

Setup

bash
cd examples/react
yarn install
yarn start

Vue.js Example

A complete Vue.js application with composition API.

Features

  • Vue 3 Composition API
  • Pinia for state management
  • Form validation
  • Data tables
  • Error handling

Implementation

vue:examples/vue/src/App.vue
<template>
  <div id="app">
    <header v-if="user">
      <h1>ERPax Frontend</h1>
      <button @click="handleLogout">Logout</button>
    </header>
    <Login v-if="!user" @login="handleLogin" />
    <main v-else>
      <InvoiceForm />
      <InvoiceList />
    </main>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useAuthStore } from './stores/auth';
import Login from './components/Login.vue';
import InvoiceForm from './components/InvoiceForm.vue';
import InvoiceList from './components/InvoiceList.vue';
const authStore = useAuthStore();
const user = ref(null);
onMounted(async () => {
  await authStore.checkAuth();
  user.value = authStore.user;
});
async function handleLogin(email, password) {
  await authStore.login(email, password);
  user.value = authStore.user;
}
async function handleLogout() {
  await authStore.logout();
  user.value = null;
}
</script>
vue
<template>
  <div>
    <h2>Invoices</h2>
    <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 setup>
import { ref, onMounted } from 'vue';
import { useInvoiceStore } from '../stores/invoices';
const invoiceStore = useInvoiceStore();
const invoices = ref([]);
const loading = ref(true);
onMounted(async () => {
  await invoiceStore.loadInvoices();
  invoices.value = invoiceStore.invoices;
  loading.value = false;
});
</script>

Setup

bash
cd examples/vue
yarn install
yarn dev

Configuration

All examples require configuration:

  1. Set your ERPax API URL:
    • Vanilla JS: Edit config.js
    • React: Edit .env or src/config.js
    • Vue: Edit .env or src/config.js
  2. Update the base URL:
    javascript
    const API_BASE_URL = 'https://your-company.erpax.com';
    // or for development: 'http://localhost:3000'

Authentication

Examples use session-based authentication. You'll need:

  1. Valid ERPax account credentials
  2. Access to your ERPax instance
  3. CORS configured (for cross-origin requests)

Documentation

All examples follow patterns from:

Notes

  • Examples are completely detached - no backend code required
  • All examples use only the RESTful JSON API
  • Examples demonstrate best practices from the documentation
  • Code is commented and follows documentation patterns

Ready to build your frontend? Start with the Frontend Development Guide for complete implementation details.

Released under an open source license.