API routing overview
Open Mercato exposes REST endpoints by discovering files under src/modules/<module>/api/<method>/<path>.ts (or their package equivalents). Each file exports a default handler and optional metadata.
Discovery & routing
- Routes are indexed by
method + pathduringnpm run modules:prepareand stored inmodules.generated.ts. - The global dispatcher lives in
src/app/api/[...slug]/route.ts. It normalises the request path, looks up the handler, and instantiates a request container. - Metadata controls authentication and authorisation:
requireAuth: booleanrequireFeatures: string[]requireRoles: string[]
Handler signature
import type { ApiHandler } from '@open-mercato/shared/modules/api/types';
const handler: ApiHandler = async ({ request, container, organization, user }) => {
const result = await container.resolve('inventoryService').list({ organizationId: organization.id });
return Response.json({ items: result });
};
export const metadata = {
requireAuth: true,
requireFeatures: ['inventory_items.view'],
};
export default handler;
Responses & errors
- Use
Response.json()or helpers from@open-mercato/shared/modules/api/responsesfor consistent status codes. - Throw
HttpErrorfrom@open-mercato/shared/modules/errorsto send typed errors. - Log contextual information via the request logger:
container.resolve('logger').info({ ... }, 'message').
Testing
- Import the handler and execute it with a mock container using Awilix’s
createContainerin your Jest tests. - Use the API data fetching tutorial for an end-to-end walkthrough.
Most CRUD operations can rely on the CRUD factory, but understanding the routing layer ensures you can always drop down to custom implementations when necessary.
Documentation
- Run
npm run modules:prepare(or the wider build scripts) to refreshmodules.generated.ts; the OpenAPI document is derived from this registry. - The generated spec is exposed at:
GET /api/docs/openapi– OpenAPI 3.1 JSON (consumable by Swagger UI, Postman, etc.).GET /api/docs/markdown– Markdown-flavoured documentation suited for quick sharing and LLM ingestion.
/backend– Dashboard card with quick links to the official docs and downloadable OpenAPI exports./backend/api-docs– Back-office page listing the same resources without feature gating.- Each API route may export
openApimetadata to enrich the spec. For file-based routes (route.ts), export anopenApiobject with Zod schemas describing query params, bodies, responses, and custom cURL examples:
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
import { z } from 'zod'
export const openApi: OpenApiRouteDoc = {
tag: 'Sales',
methods: {
POST: {
summary: 'Create tax rate',
requestBody: { schema: z.object({ name: z.string(), rate: z.number() }) },
responses: [{ status: 201, schema: z.object({ id: z.string().uuid() }) }],
},
},
}
Leaving openApi undefined still emits an operation, but providing schemas unlocks typed examples, Markdown tables, and better client generation.
Profiling CRUD latency
You can inspect where time is spent inside CRUD routes (including query engine calls and custom-field decoration) by enabling the built-in profiler:
- Set
OM_PROFILE(orNEXT_PUBLIC_OM_PROFILEwhen debugging from the browser) to a comma-separated list that matches resource names or entity ids. Use*/allfor everything, or prefixes likecustomers.*. Legacy flags (OM_CRUD_PROFILE,OM_QE_PROFILE) remain supported but are no longer required. - Run the server (
npm run dev,npm run start, etc.) with the variable exported, then trigger the slow request. - The server logs emit a single
[crud:profile]JSON snapshot at the end of each profiled request. It now includes atreearray so nested operations (like the query engine) appear under their parent step. Typical nodes includerequest_received,query_engine,custom_fields_complete, andafter_list_hook. - When the hybrid query engine runs outside of CRUD (e.g., via CLI usage) it continues to log
[qe:profile]entries using the same filters. Inside CRUD it attaches its timings to the parent snapshot so you can inspect the whole request in one place.
Performance notes
- Hybrid query indexes: CRUD list routes that declare
entityId/fieldsautomatically hit the query index engine. Keep indexes fresh viaQUERY_INDEX_AUTO_REINDEX=true, and monitor partial coverage warnings (FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES) to avoid falling back to slower base queries. - Scope-aware filtering: Most bottlenecks come from wide org/tenant scopes. Narrow filters (date ranges, search terms, tag constraints) before exporting large datasets to limit the amount of data the engine hydrates.
- Custom fields: Every list request decorates records by loading field definitions per entity/tenant/org. Large custom field catalogs or high cardinality definitions can add ~10–50 ms per batch. Prefer targeted
customFieldSourcesand archive unused definitions to keep lookups lean. For heavy exports, consider omittingcustomFieldsor caching the derived values in your module. - Instrumentation first: Enable
OM_PROFILEto identify the slow step (query execution, transformation, custom field decoration, hooks). Address the specific hot spot rather than applying blanket optimisations.