> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Add a Custom API Route

> Add a backend endpoint, regenerate the route map, and call it from a panel page with full type safety.

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.

<Info>
  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](/rc/resources/ai/overview).
</Info>

## 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

<Steps>
  <Step title="Create the route">
    API routes follow Medusa's file conventions inside your API package. The URL path mirrors the directory path.

    ```typescript packages/api/src/api/vendor/sales-summary/route.ts theme={null}
    import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
    import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

    export async function GET(
      req: AuthenticatedMedusaRequest,
      res: MedusaResponse
    ) {
      const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

      const { data: orders } = await query.graph({
        entity: "order",
        fields: ["id"],
        filters: { seller_id: req.auth_context.actor_id },
      })

      res.json({ order_count: orders.length })
    }
    ```

    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.
  </Step>

  <Step title="Regenerate the route map">
    Run codegen to scan your route files.

    ```bash Terminal theme={null}
    bunx @mercurjs/cli@latest codegen
    ```

    Codegen rewrites the generated `Routes` type that your panel apps already import:

    ```typescript apps/vendor/src/lib/client.ts theme={null}
    import { createClient, type InferClient } from "@mercurjs/client"
    import type { Routes } from "@acme/api/_generated"

    declare const __BACKEND_URL__: string

    export const client: InferClient<Routes> = createClient({
      baseUrl: __BACKEND_URL__,
      fetchOptions: { credentials: "include" },
    })
    ```

    This file ships with the starter template, so you don't need to touch it. After codegen, `client.vendor.salesSummary` simply exists, typed.

    <Tip>
      Run `bunx @mercurjs/cli@latest codegen --watch` during development so the route map regenerates as you edit route files.
    </Tip>
  </Step>

  <Step title="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).

    ```tsx apps/vendor/src/routes/sales-summary/page.tsx theme={null}
    import { useQuery } from "@tanstack/react-query"
    import { Container, Heading, Text } from "@medusajs/ui"
    import { ChartBar } from "@medusajs/icons"
    import type { RouteConfig } from "@mercurjs/dashboard-sdk"
    import type { InferClientOutput } from "@mercurjs/client"
    import { client } from "../../lib/client"

    export const config: RouteConfig = {
      label: "Sales summary",
      icon: ChartBar,
    }

    type Summary = InferClientOutput<typeof client.vendor.salesSummary.query>

    export default function SalesSummaryPage() {
      const { data } = useQuery<Summary>({
        queryKey: ["sales-summary"],
        queryFn: () => client.vendor.salesSummary.query(),
      })

      return (
        <Container className="divide-y p-0">
          <div className="flex items-center justify-between px-6 py-4">
            <Heading>Sales summary</Heading>
          </div>
          <div className="px-6 py-4">
            <Text size="small" className="text-ui-fg-subtle">
              Orders: {data?.order_count ?? "-"}
            </Text>
          </div>
        </Container>
      )
    }
    ```

    `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.
  </Step>
</Steps>

## Verify

Start the project and log into the vendor portal.

```bash Terminal theme={null}
bun run dev
```

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

<AccordionGroup>
  <Accordion title="How do path parameters work in the client?">
    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).
  </Accordion>

  <Accordion title="How do I handle errors from the client?">
    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.
  </Accordion>

  <Accordion title="Where do request validation schemas go?">
    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.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="API conventions" href="/references/api/conventions">
    Authentication, pagination, field selection, and error shapes.
  </Card>

  <Card title="Extend a workflow" href="/rc/resources/customization/extend-a-workflow">
    Put multi-step business logic behind your endpoint with rollback support.
  </Card>
</CardGroup>
