---
title: Expose a service through routes
description: Keep HTTP separate from domain behavior and map explicit URL methods to public service functions.
pagination: false
sidebar:
  icon: route
---

Use a service to expose application operations and a route to give an operation an HTTP method and URL. A route says which operation is
reachable at a particular URL and HTTP method. Adding a function to `service.ts` does not create a URL;
adding a domain does not create a server or a route prefix.

Keep the route files under `src/routes/`, outside the domains. This page covers the service-to-HTTP
boundary; [HTTP Routes getting started](/docs/modules/http) introduces request parsing, middleware, responses,
and client contracts.

## Assign each responsibility once

| Concern                                                     | Owner                                      |
| ----------------------------------------------------------- | ------------------------------------------ |
| Business input, output, errors, and behavior                | The domain function.                       |
| Which domain functions callers may use                      | The domain's `service.ts`.                 |
| URL path and HTTP method                                    | The route file path and its named exports. |
| Headers, cookies, query/body mapping, response status       | The route's transport configuration.       |
| HTTP middleware and request authentication                  | The Routes layer.                          |
| Rules that must also apply to jobs, tools, and direct calls | The function, not only HTTP middleware.    |

Routes import `@app/services/orders/service.js`, never Orders' private function files. Domain functions do not
import routes. That direction lets you add another transport without pulling HTTP concerns into the
business operation.

## Map methods to public functions

With an existing Orders service exposing `searchOrders` and `createOrder`, scaffold a new route:

```sh title="Terminal — application root"
bun run relkit add route /orders --mode service-route --service orders \
  --map GET=searchOrders --map POST=createOrder
```

This creates `src/routes/orders/route.ts` and maps both methods. If that file already exists,
edit it instead. The command uses inferred mapping and HTTP `200`; add the explicit request
mapping and `201` status in source as shown below. See the
[route command reference](/docs/api/cli#relkit-add-route-path) for all flags.

Use `defineServiceRoutes` when the methods in a route file belong to one service:

```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,
  },
});
```

The file supplies `/orders`. The exported `GET` and `POST` names supply the methods. `GET` selects the
public `searchOrders` function by member name. `POST` selects `createOrder`, explicitly maps headers
and body fields into its domain input, and sets the successful response status to `201`.

### Use the service routes 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.

Use a string member name when inferred request/response mapping fits. Use an object with `member` when
you need explicit mapping, status, limits, or other function-route options. Explicit request mappings
replace inference rather than partially extending it. See [request mapping](/docs/modules/http/requests).

Keep the exported destructuring form shown above. `defineServiceRoutes` does not support renamed
method bindings, rest bindings, a default export, or methods named after business operations. Supported
methods are `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS`.

## Choose the URL independently of the domain

| Route file                             | URL                | What it decides                                                    |
| -------------------------------------- | ------------------ | ------------------------------------------------------------------ |
| `src/routes/orders/route.ts`           | `/orders`          | Collection operations.                                             |
| `src/routes/orders/[orderId]/route.ts` | `/orders/:orderId` | Operations on a particular order.                                  |
| `src/routes/uploads/route.ts`          | `/uploads`         | A URL that calls the Assets service without using its domain name. |

The service does not own these URL prefixes. Several route files can use the same service, and a route
module can import multiple public services if necessary. Keep reusable cross-domain business
orchestration in a function instead of spreading it across HTTP handlers.

Follow [file-system routes](/docs/modules/http/routes) for dynamic segments, catch-all paths, matching precedence,
and route identities. Do not add `path` or `method` to a function-backed `defineRoute` descriptor.

## When to use defineRoute

For an individual mapping, use `defineRoute` with a public function as its `target`:

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

export const GET = defineRoute({
  target: hello.hello,
});
```

Both factories produce route descriptors for the same routing pipeline. Neither can target an event
contract, an event-only subscriber, a tool, or an agent directly. To expose agent behavior, route to the
[ordinary function that calls it](/docs/modules/service/organization#tools-and-agents-stay-with-their-business-domain).

`defineRoute({ handler: ... })` also supports raw HTTP integrations. Keep business operations on the
function-backed path. Better Auth is a specialized raw-handler integration mounted on an `ALL`
catch-all route; `ALL` is not a method accepted by `defineServiceRoutes`.

## Use from the frontend

After [frontend setup and contract pull](/guides/frontend/connect-react), render the Hello route
through its generated `GET /hello` selector. `name` and `data.message` are inferred from the
backend function's schemas:

```tsx title="src/App.tsx — frontend"
"use client";

/** Renders the minimal backend's greeting through the pulled registry and a same-origin client provider. */
import { RelkitClientProvider, useRelkitClient, useRoute } from "@relkit/client/react";

/** Displays the typed Hello result after the provider resolves backend identity.
 * @returns Loading, connection failure, request failure, or the successful greeting. */
export function Greeting() {
  const { status } = useRelkitClient();
  const greeting = useRoute("GET /hello", { input: { name: "RELKIT" } });

  if (status === "application-updated") {
    return <p role="alert">The API changed. Pull its contract and restart the frontend.</p>;
  }
  if (status === "error") return <p role="alert">Cannot connect to the API.</p>;
  if (greeting.isError) return <p role="alert">{greeting.error.message}</p>;
  if (greeting.isPending) return <p role="status">Loading greeting…</p>;
  return <h1>{greeting.data.message}</h1>;
}

/** Supplies the shared query cache and identity handshake to the frontend tree.
 * @returns The greeting inside a same-origin RELKIT provider. */
export default function App() {
  return (
    <RelkitClientProvider>
      <Greeting />
    </RelkitClientProvider>
  );
}
```

Keep `.invoke()` and service imports on the backend. A function or service needs a client-exposed
route before the browser can call it. The provider handles identity and query state; the
[Frontend guide](/guides/frontend/connect-react) also shows where this component belongs in Next.js.

Reference: [`useRoute`](/docs/api/client#useroute),
[`RelkitClientProvider`](/docs/api/client#relkitclientprovider).

`defineServiceRoutes` produces the same client contracts per exported method: `GET /orders` uses
[`useRoute`](/docs/modules/client/routes), while `POST /orders` uses
[`useRouteMutation`](/docs/modules/client/mutations). The input remains the target function's input;
explicit HTTP header or body mappings do not construct RPC input. Only exposing a service member
does not make it a browser operation: it needs a client-exposed route.

## Continue with the Routes topics

Choose the topic that matches the HTTP work you need to do:

[Your first HTTP route](/docs/modules/http/first-route): add a validated endpoint and
integration test.

[Define a route](/docs/modules/http/define): choose `defineRoute` or `defineServiceRoutes`.

[File-system routes](/docs/modules/http/routes): choose paths and methods.

[Requests](/docs/modules/http/requests) and [uploads](/docs/modules/http/uploads): map transport
values into input.

[Middleware](/docs/modules/http/middleware) and [rate limits](/docs/modules/http/rate-limits):
apply HTTP policy.

[Responses](/docs/modules/http/responses): map results and declared errors into HTTP responses.

[Raw handlers](/docs/modules/http/raw-handlers): mount adapters that own their HTTP protocol.

[OpenAPI](/docs/modules/http/openapi) and [generated
clients](/docs/modules/http/generated-clients): use the checked contracts outside the server,
with REST and RPC behavior distinguished.

These transport guides build on the same domain organization; they do not introduce a second place
for business handlers.

## Verify routing separately from behavior

From your application's root, run `bun run check` after adding a method. Expect the route to appear in
the checked graph and OpenAPI output. Test the function directly for its domain behavior, then use an
HTTP integration test for mapping, authentication, and status codes.

For a running app containing the Hello route above:

```sh
curl -i http://localhost:3000/hello\?name=RELKIT
```

Expect HTTP `200` and `{ "message": "Hello, RELKIT!" }`. Use your configured backend port if it differs.
The [first-service example](/docs/modules/service/first-service) includes the source and test steps.

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

**Reference and source**

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

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