---
title: Define a route
description: Connect named HTTP methods to public service functions with defineRoute or defineServiceRoutes.
pagination: false
sidebar:
  icon: route
---

Define a route to connect an HTTP method and path to a public service operation. Import the target through its domain's
`service.ts`; never reach into another domain's private function directory from a route.
The [file-system conventions](/docs/modules/http/routes) determine the URL and allowed method exports.

## Scaffold a route

Run these commands from an installed application's root. Choose the mode that matches the
endpoint you are creating; then edit the generated file using the API examples below.

**Service function**

To route through existing public functions, use service-route mode. With `details` and
`updateDetails` public callable members in the `users` service:

```sh
bun run relkit add route /users/:id/details \
  --mode service-route --service users \
  --map GET=details --map PATCH=updateDetails
```

This creates `src/routes/users/[id]/details/route.ts` with `defineServiceRoutes`. The
mapped names are public service member names, not private filenames or event IDs. Headless
service-route commands need at least one `--map`; supported methods are `GET`, `POST`,
`PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS`. `ALL` is auth-only.

**Raw handler**

From an installed application's root, create a standalone raw handler:

```sh
bun run relkit add route /status --mode route
```

This creates `src/routes/status/route.ts` with a raw `GET` returning `{ ok: true }`.
It needs no service or function. It appears in OpenAPI/Scalar with a generic response contract,
not an inferred function response schema. `--map` is not accepted in this mode.

For a complete function-backed endpoint, follow [Add an Orders endpoint](/docs/learn/first-route).
The CLI creates the method mapping; add request mapping, response status, and policy in source.

| URL passed to the command | Generated file                           |
| ------------------------- | ---------------------------------------- |
| `/`                       | `src/routes/route.ts`                    |
| `/users/:id/details`      | `src/routes/users/[id]/details/route.ts` |
| `/files/*parts`           | `src/routes/files/[...parts]/route.ts`   |
| `/files/*parts?`          | `src/routes/files/[[...parts]]/route.ts` |

Write route paths without quotes. Escape literal shell wildcards, for example `/files/\*parts`
or `/files/\*parts\?`, so the shell passes them to the CLI unchanged. Existing target files are never
overwritten; edit an existing route to add another method. Run `bun run check`, then verify
the endpoint and its entry in `/_relkit/v1/openapi.json` while dev is running. The
[Scalar UI](/docs/modules/http/openapi) is available when explicitly selected.

For every route scaffolding flag, see the [route command reference](/docs/api/cli#relkit-add-route-path).

## Expose one public function

Use `defineRoute` for one method-to-function mapping:

```ts title="src/routes/echo/route.ts"
/** Maps the HTTP endpoint to its owning service's public member. */
import { defineRoute } from "@relkit/app/routes";
import echo from "@app/services/echo/service.js";

export const POST = defineRoute({
  target: echo.echo,
});
```

This is `POST /echo`. Omit `request` and `responses` when the input schema and output describe ordinary
JSON correctly. The descriptor selects behavior; it does not call `echo.echo.invoke()` at module load.

A target must be an ordinary callable function exposed by a service. An event contract, event-only
subscriber, tool, or agent is not a route target. To expose agent behavior, use a public
[function that calls the agent](/docs/modules/service/organization#tools-and-agents-stay-with-their-business-domain).

### Use from the frontend

After [frontend installation, contract pull, and provider setup](/guides/frontend/connect-react),
call this `POST /echo` operation with its domain input. The route produces both the React selector
`POST /echo` and the TypeScript operation `route.post.echo`.

**React**

Render this component under `RelkitClientProvider`. Clicking the button sends `{ message }` and
displays the inferred `{ message: string }` result:

```tsx title="src/EchoButton.tsx"
"use client";

/** Calls the documented POST /echo function route after an explicit user action. */
import { useRelkitClient, useRouteMutation } from "@relkit/client/react";

/** Sends domain input and renders the validated output; the hook owns mutation state.
 * @returns An Echo button with pending, failure, and success feedback. */
export function EchoButton() {
  const { status } = useRelkitClient();
  const echo = useRouteMutation("POST /echo");

  return (
    <section>
      <button
        disabled={status !== "ready" || echo.isPending}
        onClick={() => echo.mutate({ message: "Hello from the frontend" })}
      >
        Send message
      </button>
      {echo.isPending && <p role="status">Sending…</p>}
      {echo.isError && <p role="alert">{String(echo.error)}</p>}
      {echo.isSuccess && <p>{echo.data.message}</p>}
    </section>
  );
}
```

**TypeScript**

Without React, load the browser identity before the mutation and call the generated operation:

```ts title="src/echo-client.ts"
/** Calls the Echo contract from a TypeScript browser frontend without React or backend source imports. */
import { createClient } from "@relkit/client";

/** Sends one message over RPC using types from the caller's pulled contract.
 * @param baseUrl - Public backend origin, or the frontend origin when proxying RPC.
 * @param message - Nonempty message accepted by the Echo function.
 * @returns The validated message output; transport and application failures reject. */
export async function echoMessage(baseUrl: string, message: string) {
  const response = await fetch(new URL("/_relkit/v1/client/identity", baseUrl), {
    credentials: "include",
    cache: "no-store",
  });
  if (!response.ok) throw new Error(`Identity request failed (${response.status})`);
  const identity = await response.json();
  if (
    !identity ||
    typeof identity.identityScope !== "string" ||
    typeof identity.sessionEpoch !== "string"
  ) {
    throw new Error("The backend returned an invalid client identity");
  }
  const client = createClient({
    baseUrl,
    headers: {
      "x-relkit-identity-scope": identity.identityScope,
      "x-relkit-session-epoch": identity.sessionEpoch,
    },
  });
  return client["route.post.echo"]({ message });
}
```

The React provider performs this identity step for hooks.

These calls use RPC over `/rpc`, with the same authorization and policy. Pass domain input rather
than assembling the route's REST mapping. Raw handlers and routes with `client: false` do not
generate these operations. Never import the backend descriptor into the browser.

Reference: [`useRouteMutation`](/docs/api/client#useroutemutation),
[`createClient`](/docs/api/client#createclient). Continue with [mutations](/docs/modules/client/mutations).

## Group methods that use one service

Use `defineServiceRoutes` to select public function members by name:

```ts title="examples/commerce/src/routes/orders/route.ts"
/**
 * Maps order HTTP operations to public service members with the existing request
 * and rate-limit policies. The rate-limit reference addresses the declared cache
 * by its stable identity without importing a private service implementation.
 */
import { defineServiceRoutes, http } from "@relkit/app/routes";
import { z } from "@relkit/app/schema";
import orders from "../../services/orders/service.js";

export const { GET, POST } = defineServiceRoutes(orders, {
  GET: "searchOrders",
  POST: {
    member: "createOrder",
    request: http.input({
      orderId: http.header("idempotency-key"),
      customerEmail: http.header("x-customer-email"),
      sku: http.body("sku"),
      quantity: http.body("quantity"),
    }),
    rateLimit: {
      limit: 20,
      windowMs: 60_000,
      key: http.header("x-customer-email"),
      store: {
        ref: { kind: "cache", id: "orders.rate-limits" },
        key: z.string(),
        value: z.number().int().nonnegative(),
      },
    },
    successStatus: 201,
  },
});
```

`GET: "searchOrders"` uses inference. The `POST` object selects `createOrder` and adds HTTP-specific
mapping and a `201` success status. Both forms produce the same kind of checked function route.

### Use both methods from the frontend

After [frontend setup and pulling this backend's contract](/guides/frontend/connect-react),
render this component under `RelkitClientProvider`. Each exported service-route method produces
its own typed operation: `GET /orders` reads the search count, while `POST /orders` creates an order.

```tsx title="src/Orders.tsx"
"use client";

/** Calls both commerce service-route methods using their generated domain inputs inside the frontend provider. */
import { useRelkitClient, useRoute, useRouteMutation } from "@relkit/client/react";

/** Reads the Orders search count and submits one explicit creation without replaying a failed write.
 * @returns Typed query results, creation feedback, and an action available after identity is ready. */
export function Orders() {
  const { status } = useRelkitClient();
  const orders = useRoute("GET /orders", { input: { status: "pending" } });
  const create = useRouteMutation("POST /orders");

  return (
    <section>
      {orders.isPending && <p role="status">Loading orders…</p>}
      {orders.data && <p>{orders.data.count} orders</p>}
      {orders.isError && <p role="alert">{orders.error.message}</p>}
      <button
        disabled={status !== "ready" || !create.isIdle}
        onClick={() =>
          create.mutate({
            orderId: crypto.randomUUID(),
            customerEmail: "reader@example.com",
            sku: "book",
            quantity: 1,
          })
        }
      >
        Create an order
      </button>
      {create.isPending && <p role="status">Creating…</p>}
      {create.isSuccess && <p>Created: {create.data.orderId}</p>}
      {create.isError && (
        <p role="alert">{create.error.message}. Check the outcome before retrying.</p>
      )}
    </section>
  );
}
```

`status`, `count`, and the creation input come from the selected functions' schemas. RPC receives
`orderId` and `customerEmail` as input fields even though REST maps them from headers. A service's
public member alone is not a browser endpoint: the client-exposed route supplies that boundary.

Reference: [`useRoute`](/docs/api/client#useroute), [`useRouteMutation`](/docs/api/client#useroutemutation).
See [request mapping](/docs/modules/http/requests) for the REST side and
[mutations](/docs/modules/client/mutations) for submission and recovery behavior.

Keep the exported destructuring form. Supported keys are `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,
`HEAD`, and `OPTIONS`; do not rename the bindings, add a rest binding, or use a default export.
`ALL` is reserved for [raw handlers](/docs/modules/http/raw-handlers), not `defineServiceRoutes`.

Several route files can use the same service. Individual `defineRoute` exports in one file can target
different public services. Put reusable cross-domain orchestration in a domain function instead of
creating business logic inside the route module.

## Add only the transport options you need

| Option                               | Use it when…                                                                                                                                       |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request`                            | Inference cannot express headers, cookies, names, or nesting. It replaces all inferred input mapping. See [requests](/docs/modules/http/requests). |
| `accept`                             | The route consumes `multipart/form-data` instead of JSON. See [uploads](/docs/modules/http/uploads).                                               |
| `successStatus`                      | One successful JSON operation needs a different `2xx` status, such as `201`.                                                                       |
| `responses`                          | You need a complete explicit response contract. It replaces inference; see [responses](/docs/modules/http/responses).                              |
| `maxBodyBytes`                       | Parsing this route needs a specific positive byte ceiling.                                                                                         |
| `timeoutMs`                          | The function invocation needs a route-specific positive timeout.                                                                                   |
| `rateLimit`                          | Requests need a keyed limit before mapping and invocation. See [rate limits](/docs/modules/http/rate-limits).                                      |
| `id`, `title`, `description`, `tags` | You need stable operation identity or human-facing metadata.                                                                                       |

Do not add `path` or `method`: the compiler derives both from source. There is no service-level URL
prefix or middleware property. The `auth` option is specific to a recognized Better Auth raw handler,
not a generic authorization flag for a function route.

## Check the new operation

From the application root:

```sh
bun run check
bun run typecheck
```

Resolve diagnostics before starting the server. Check the generated route method, path, target,
input mapping, and responses. Then test the endpoint itself: a direct function test does not exercise
HTTP parsing or policy. The [first-route walkthrough](/docs/modules/http/first-route) includes both the source
and an in-process HTTP test.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/routes` API](/docs/api/routes)
- [`@relkit/services` API](/docs/api/services)

- [`examples/commerce/src/routes/orders/route.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/routes/orders/route.ts)
