Skip to main content
Mercur’s typed API client is generated from your actual route files, not hand-maintained. A custom endpoint you add to the API package becomes a first-class, fully typed client call after one codegen run. This tutorial walks the whole loop: route, then codegen, then a typed call from a custom panel page.
The contract is generated, not declared. You never write an interface for your endpoint. mercurjs codegen reads the route’s handler and validators and emits the Routes type the client consumes, so the panel call site breaks at compile time the moment the backend changes. This loop is also what makes Mercur projects reliable targets for AI agents. See Building with AI.

What you’ll build

A GET /vendor/sales-summary endpoint returns the seller’s order count. You call it from a custom vendor portal page via client.vendor.salesSummary.query() with inferred types.

Build the loop

1

Create the route

API routes follow Medusa’s file conventions inside your API package. The URL path mirrors the directory path.
packages/api/src/api/vendor/sales-summary/route.ts
Routes under src/api/vendor/* run behind the vendor authentication middleware, so req.auth_context identifies the calling seller. Use src/api/admin/* for operator endpoints and src/api/store/* for public storefront endpoints.
2

Regenerate the route map

Run codegen to scan your route files.
Terminal
Codegen rewrites the generated Routes type that your panel apps already import:
apps/vendor/src/lib/client.ts
This file ships with the starter template, so you don’t need to touch it. After codegen, client.vendor.salesSummary simply exists, typed.
Run bunx @mercurjs/cli@latest codegen --watch during development so the route map regenerates as you edit route files.
3

Call it from a panel page

Drop a page into the vendor app and call the endpoint through the client. Route segments map to camelCase properties, and the terminal call chooses the HTTP method: query (GET), mutate (POST), or delete (DELETE).
apps/vendor/src/routes/sales-summary/page.tsx
InferClientOutput extracts the response type straight from the client method. Change the route’s response shape, rerun codegen, and this component stops compiling until you update it.

Verify

Start the project and log into the vendor portal.
Terminal
Confirm each of the following:
  1. Sidebar entry: Sales summary appears in the sidebar (the config export registered it), and the page shows the order count.
  2. Auth guard: curl http://localhost:9000/vendor/sales-summary without a token returns an authentication error. The vendor middleware guards your route.
  3. Generated contract: change the route to return { count: ... } instead of { order_count: ... }, rerun codegen, and confirm the page fails to type-check. That is the generated contract doing its job. Revert after.

FAQ

Use $-prefixed segments: a route at src/api/vendor/things/[id]/route.ts is called as client.vendor.things.$id.query({ $id: "thing_123" }). The $id key is threaded into the URL path, and everything else in the object becomes query params (GET) or the JSON body (POST).
Failed requests throw ClientError from @mercurjs/client, carrying status, statusText, and the backend’s message. Wrap calls in try/catch or let TanStack Query surface the error.
Follow Medusa conventions: a validators.ts next to the route with a Zod schema, wired through the route’s middleware. Codegen reads validators too, so the client’s input type reflects them.

Next steps

API conventions

Authentication, pagination, field selection, and error shapes.

Extend a workflow

Put multi-step business logic behind your endpoint with rollback support.