RatchetRatchet

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

MethodPath
GET/api/:modellist, with filtering/sorting/pagination
GET/api/:model/:idfetch one
POST/api/:modelcreate — runs the model's create pipeline
PATCH/api/:model/:idupdate — runs the model's update pipeline
DELETE/api/:model/:idremove — 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; response meta is { total, limit, offset }. This is the default, and a bare ?sort= stays in it (just ordered).
  • ?cursor= — cursor-mode pagination; response meta is { nextCursor, hasMore }. Requires an accompanying single-key ?sort=; pass ?cursor= empty for the first page, then the returned nextCursor for 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 desc

Only 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=paid

For 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=customer

include 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=true

By 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:

CodeStatus
VALIDATION_ERROR400body failed the model's Zod schema, or a malformed query param
MODEL_NOT_FOUND404:model isn't in the registry
NOT_FOUND404no row for the given :id
INVALID_INCLUDE400unknown or nested ?include= relation
UNFILTERABLE_FIELD / UNSORTABLE_FIELD400field isn't indexed: true
INVALID_OPERATOR400operator 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));

On this page