Defines the server-side data contract. The frontend consumes this API via
[store.connect]storage connectors. See STATE_AND_ROUTING.md for how the frontend binds to these endpoints.
- Standard REST — resources as nouns, HTTP verbs for actions.
- JSON Schema via OPTIONS — every entity endpoint exposes its schema and
allowed methods on
OPTIONSrequests, enabling automated form generation. - Field-level validation errors — server returns
422with per-field error messages that map directly to form fields. - SSE for realtime — a single
/api/eventsstream pushes entity change notifications to connected clients. - Stateless requests — no server-side sessions. Auth via tokens if needed.
/api/:entity OPTIONS (schema + methods), GET (list), POST (create)
/api/:entity/:id OPTIONS (schema + methods), GET (read), PUT (update), DELETE (remove)
/api/events GET (SSE stream)
All request/response bodies are application/json.
An OPTIONS request to any entity endpoint returns the JSON Schema and
allowed HTTP methods. This is the primary mechanism for schema-driven forms.
{
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Project",
"type": "object",
"required": ["name"],
"properties": { ... }
},
"methods": ["OPTIONS", "GET", "POST"]
}{
"schema": { ... },
"methods": ["OPTIONS", "GET", "PUT", "DELETE"]
}The Allow header is also set for HTTP compliance.
GET /api/:entity?schema=true is still supported as a convenience.
The frontend can fetch the schema at runtime and generate form fields automatically. The schema provides:
| JSON Schema keyword | Form behavior |
|---|---|
type: "string" |
<input type="text"> |
format: "email" |
<input type="email"> |
format: "date-time" |
<input type="datetime-local"> |
enum: [...] |
<select> with options |
minLength / maxLength |
Validation constraints |
readOnly: true |
Field displayed but not editable |
required: [...] |
Native required attribute + visual indicator |
JSON Schema constraints map directly to HTML validation attributes:
| JSON Schema | HTML attribute |
|---|---|
minLength |
minlength |
maxLength |
maxlength |
minimum |
min |
maximum |
max |
pattern |
pattern |
The browser's native constraint validation API enforces these — no custom JS validation needed for client-side checks.
Some validation can only happen server-side (uniqueness, business rules).
When validation fails, the server returns 422 with field-level errors:
{
"error": "Validation failed",
"fields": {
"name": "name is required",
"status": "Must be one of: active, archived"
}
}The schema-form component maps fields entries to the corresponding
form-field components, which display the error below the input.
| Status | Body | Meaning |
|---|---|---|
422 |
{ error, fields } |
Validation failed — fields maps names to messages |
404 |
{ error } |
Entity or collection not found |
201 |
Entity JSON | Created successfully |
200 |
Entity JSON | Updated successfully |
204 |
Empty | Deleted successfully |
This allows a single generic schema-form component to render any entity's
create/edit form without entity-specific template code.
Query params for filtering, sorting, pagination:
| Param | Example | Purpose |
|---|---|---|
limit |
?limit=20 |
Page size (default: 20) |
offset |
?offset=40 |
Skip N records |
sort |
?sort=-createdAt |
Sort field, - prefix for desc |
filter |
?role=admin |
Field equality filter |
Response:
{
"data": [ { "id": "1", "firstName": "Jane", ... } ],
"total": 42,
"limit": 20,
"offset": 0
}Returns a single entity object:
{ "id": "1", "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com" }Returns 404 if not found.
Request body: entity fields (without id or readOnly fields).
Response: the created entity with id and server-generated fields.
Status: 201 Created.
Request body: full or partial entity fields.
Response: the updated entity.
Status: 200 OK.
Response: 204 No Content.
Returns a text/event-stream connection. The server pushes events when
entities are created, updated, or deleted.
event: update
data: {"type":"user","id":"1","action":"updated"}
event: update
data: {"type":"user","id":"3","action":"created"}
event: update
data: {"type":"user","id":"2","action":"deleted"}
| Field | Value |
|---|---|
type |
Entity name (lowercase, singular): user, post |
id |
Entity ID that changed |
action |
created, updated, or deleted |
See STATE_AND_ROUTING.md — the connectRealtime()
utility listens to this stream and calls store.clear() on the relevant
model, triggering automatic re-fetch for any component displaying that data.
The server ships with in-memory dummy data for development. No database required.
/** @type {Map<string, Map<string, object>>} */
const db = new Map();
// Seed with dummy users
db.set(
'users',
new Map([
[
'1',
{
id: '1',
firstName: 'Jane',
lastName: 'Doe',
email: 'jane@example.com',
role: 'admin',
createdAt: '2024-01-15T09:00:00Z',
},
],
[
'2',
{
id: '2',
firstName: 'John',
lastName: 'Smith',
email: 'john@example.com',
role: 'editor',
createdAt: '2024-02-20T14:30:00Z',
},
],
[
'3',
{
id: '3',
firstName: 'Alex',
lastName: 'Chen',
email: 'alex@example.com',
role: 'viewer',
createdAt: '2024-03-10T11:15:00Z',
},
],
]),
);Schemas are defined once and served via OPTIONS:
const schemas = new Map();
schemas.set('users', {
$schema: 'https://json-schema.org/draft/2020-12/schema',
title: 'User',
type: 'object',
required: ['firstName', 'lastName', 'email'],
properties: {
/* ... as above ... */
},
});- Define the JSON Schema in the schema registry.
- Seed dummy data in the
dbMap. - The generic CRUD router handles all operations automatically.
- Create a corresponding frontend store model in
src/store/.
No entity-specific route handlers needed — the server uses a single generic router that works for any entity registered in the schema map.
| Frontend (Hybrids Store) | Backend (Express) |
|---|---|
[store.connect].get(id) |
GET /api/:entity/:id |
[store.connect].set(id, values) |
PUT /api/:entity/:id |
[store.connect].list(params) |
GET /api/:entity?... |
store.set(model, null) (delete) |
DELETE /api/:entity/:id |
| Schema fetch for forms | OPTIONS /api/:entity |
| Schema fetch for item | OPTIONS /api/:entity/:id |
| Realtime invalidation | GET /api/events (SSE) |
This contract means adding a new entity type requires:
- One JSON Schema definition (server)
- One store model file (frontend)
- Dummy seed data (server, for dev)
Everything else — CRUD routes, form generation, cache invalidation — is handled by the generic infrastructure.