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
- Update Configuration Edit
config.jsand set your ERPax API URL:javascriptconst CONFIG = { API_BASE_URL: 'https://your-company.erpax.com', }; - Run the Examplebash
# Using Python python3 -m http.server 8000 # Using Node.js npx http-server -p 8000 # Then visit http://localhost:8000 - 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 configurationerpax-client.js- Complete API clientapp.js- Application logicindex.html- HTML structurestyles.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 startVue.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 devConfiguration
All examples require configuration:
- Set your ERPax API URL:
- Vanilla JS: Edit
config.js - React: Edit
.envorsrc/config.js - Vue: Edit
.envorsrc/config.js
- Vanilla JS: Edit
- 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:
- Valid ERPax account credentials
- Access to your ERPax instance
- CORS configured (for cross-origin requests)
Documentation
All examples follow patterns from:
- Frontend Development Guide - Complete guide
- Getting Started - Authentication setup
- API Reference - All endpoints
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.