REST API
Filtering, sorting, pagination, and `?include=` on the generic `/api/:model` router.
ratchet serve mounts one generic router at /api/:model — every model gets the same route family, dispatched by looking up :model in the registry at request time. There are no per-model generated route files.
Routes
| Method | Path | |
|---|---|---|
GET | /api/:model | list, with filtering/sorting/pagination |
GET | /api/:model/:id | fetch one |
POST | /api/:model | create — runs the model's create pipeline |
PATCH | /api/:model/:id | update — runs the model's update pipeline |
DELETE | /api/:model/:id | remove — runs the model's remove pipeline |
Every response body is { data, meta? }. Errors are { error: { code, message, fields? } } with a matching HTTP status (see Errors). Fields marked sensitive: true on the model are stripped from every response.
Listing: GET /api/:model
Pagination
?limit=— default 20, clamped (not rejected) to a max of 100.?offset=— offset-mode pagination; responsemetais{ total, limit, offset }. This is the default, and a bare?sort=stays in it (just ordered).?cursor=— cursor-mode pagination; responsemetais{ nextCursor, hasMore }. Requires an accompanying single-key?sort=; pass?cursor=empty for the first page, then the returnednextCursorfor each subsequent one.
Sorting
GET /api/invoices?sort=amount # ascending
GET /api/invoices?sort=-amount # descending, '-' prefix
GET /api/invoices?sort=status,-amount # multi-column: status asc, then amount descOnly fields declared indexed: true (plus the always-indexed system columns id, createdAt, updatedAt, createdById) can be sorted on — otherwise the API returns UNSORTABLE_FIELD. Cursor-mode pagination accepts a single sort key only.
Filtering
Simple equality filters are plain query params:
GET /api/invoices?status=paidFor anything beyond equality, pass ?filter= as a JSON array of [field, operator, value] triples:
GET /api/invoices?filter=[["amount",">=","100"],["status","!=","draft"]]Supported operators: =, !=, >, >=, <, <=, in, like, ilike, is. like/ilike are string/text only and take a pattern with explicit % wildcards (ilike is the case-insensitive form). Only fields declared indexed: true can be filtered on (UNFILTERABLE_FIELD otherwise), and each operator must be valid for the field's kind (INVALID_OPERATOR otherwise).
Relations: ?include=
GET /api/invoices?include=customerinclude takes a comma-separated list of relation names, derived by stripping the Id suffix from a field.reference key (customerId -> customer). Nested/dot-chained includes (customer.company) are rejected outright.
Soft deletes
GET /api/invoices?includeDeleted=trueBy default, soft-deleted rows (via persist.remove) are excluded from both list and single-record reads.
Errors
Every route shares the same error shape. Common codes:
| Code | Status | |
|---|---|---|
VALIDATION_ERROR | 400 | body failed the model's Zod schema, or a malformed query param |
MODEL_NOT_FOUND | 404 | :model isn't in the registry |
NOT_FOUND | 404 | no row for the given :id |
INVALID_INCLUDE | 400 | unknown or nested ?include= relation |
UNFILTERABLE_FIELD / UNSORTABLE_FIELD | 400 | field isn't indexed: true |
INVALID_OPERATOR | 400 | operator invalid, or not valid for the field's kind |
Assembling a server
Most of the time you don't mount routers by hand — createRatchetApp (@egig/ratchet/server) does it, in the one correct order, for ratchet serve and every deploy target alike:
import { createRatchetApp } from '@egig/ratchet/server';
import { bundle } from './.ratchet/app.js';
const app = await createRatchetApp({ db, bundle, storage, consoleAssets, consolePath: '/console' });See Deploying for the full option set.
Building your own router
ratchet/router also exports the individual pieces, if you need to mount them yourself (e.g. inside a custom App, or to add routes around them):
import { App, createApiRouter, buildRegistryMap } from '@egig/ratchet/router';
const registry = buildRegistryMap(registryModule); // from .ratchet/registry.ts
const app = new App();
app.route('/api', createApiRouter(registry, db));