Services Layer - cynchro/OldSchoolFrontFrame GitHub Wiki
Services Layer
Services are plain JavaScript functions that own all data-fetching and business logic. They have no access to the DOM and no knowledge of the module that calls them.
Why a services layer?
Separating HTTP calls from module code has practical benefits:
- Services can be tested in isolation without rendering HTML
- The same service can be called from multiple modules
- Module code stays focused on UI state, not API details
- Service errors are handled explicitly, making failure modes clear
File location
modules/
└── clientes/
├── clientes.html
├── clientes.js
└── services/
└── clientes.service.js ← your service file
Writing a service
A service file exports plain async functions:
// modules/clientes/services/clientes.service.js
const BASE_URL = '/api/clientes';
export async function getClientes() {
const res = await fetch(BASE_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function getCliente(id) {
const res = await fetch(`${BASE_URL}/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function createCliente(data) {
const res = await fetch(BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function deleteCliente(id) {
const res = await fetch(`${BASE_URL}/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
}
Using a service in a module
Import the service explicitly. Call it in mounted() or in methods:
// modules/clientes/clientes.js
import { defineModule } from '../../framework/module/module.js';
import { getClientes, deleteCliente } from './services/clientes.service.js';
defineModule({
state: () => ({
clientes: [],
loading: false,
error: null,
}),
methods: {
async cargar(ctx) {
ctx.state.loading = true;
ctx.state.error = null;
try {
ctx.state.clientes = await getClientes();
} catch (e) {
ctx.state.error = e.message;
} finally {
ctx.state.loading = false;
}
},
async eliminar(ctx, event) {
const id = Number(event.target.dataset.id);
await deleteCliente(id);
ctx.state.clientes = ctx.state.clientes.filter(c => c.id !== id);
},
},
mounted(ctx) {
ctx.methods.cargar(ctx);
},
});
Rules for services
| Rule | Reason |
|---|---|
| No DOM access in services | Services must be UI-agnostic |
| Return data, throw errors | Let the module decide how to display failures |
No ctx parameter |
Services don't know about module state |
| One file per feature area | clientes.service.js, auth.service.js, etc. |
| Explicit imports — no registry | Makes dependencies visible at a glance |
Services with auth headers
If your API requires authentication tokens, read them from a shared auth helper:
// components/auth/auth.js
export function getAuthHeaders() {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}` } : {};
}
// modules/pedidos/services/pedidos.service.js
import { getAuthHeaders } from '../../../components/auth/auth.js';
export async function getPedidos() {
const res = await fetch('/api/pedidos', { headers: getAuthHeaders() });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}