Skip to main content

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 + path during npm run modules:prepare and stored in modules.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: boolean
    • requireFeatures: 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/responses for consistent status codes.
  • Throw HttpError from @open-mercato/shared/modules/errors to 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 createContainer in 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 refresh modules.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 openApi metadata to enrich the spec. For file-based routes (route.ts), export an openApi object 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 (or NEXT_PUBLIC_OM_PROFILE when debugging from the browser) to a comma-separated list that matches resource names or entity ids. Use */all for everything, or prefixes like customers.*. 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 a tree array so nested operations (like the query engine) appear under their parent step. Typical nodes include request_received, query_engine, custom_fields_complete, and after_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/fields automatically hit the query index engine. Keep indexes fresh via QUERY_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 customFieldSources and archive unused definitions to keep lookups lean. For heavy exports, consider omitting customFields or caching the derived values in your module.
  • Instrumentation first: Enable OM_PROFILE to identify the slow step (query execution, transformation, custom field decoration, hooks). Address the specific hot spot rather than applying blanket optimisations.