---
title: Contract-first
description: Declare routes in a shared contract and implement them separately.
---

Import `route` from `@rest-rpc/core` to declare a route independently
of its server implementation. The contract can be shared directly with a Fetch
client and implemented with any server adapter.

The declaration methods are the same as the [Route Builder](/docs/route-builder).
The difference is that outputs are declared with schemas rather than inferred
from handlers.

## Declare Routes

```ts contract.ts
import { route } from "@rest-rpc/core";
import { z } from "zod";

const todoSchema = z.object({ id: z.string(), title: z.string() });

const create = route.input(z.object({ title: z.string() })).output(todoSchema);

const get = route
	.get("/todos/:id")
	.params(z.object({ id: z.string() }))
	.response(200, todoSchema)
	.response(404, z.object({ code: z.literal("TODO_NOT_FOUND") }));

export const api = { todos: { create, get } };
```

Use `.output()` for a plain result or `.response()` for status responses.
Streams use `.streamOutput()` or `.streamResponse()`. These schemas define
handler and client types and provide [runtime validation](/docs/http-behavior/schemas).
See [Streaming](/docs/streaming) for stream handlers.

## Implement Routes

Each server adapter exports `implement()`. Pass a route or route tree to get
builders with typed `.handler()` methods:

```ts server.ts
import { implement } from "@rest-rpc/express";
import { api } from "./contract";
import { createTodo, findTodo } from "./todos";

const implementer = implement(api);

const create = implementer.todos.create.handler(({ input: { title } }) =>
	createTodo(title),
);

const get = implementer.todos.get.handler(({ params }) => {
	const todo = findTodo(params.id);
	return todo
		? { status: 200, body: todo }
		: { status: 404, body: { code: "TODO_NOT_FOUND" } };
});

export const routes = { todos: { create, get } };
```

The implementation must match the contract's inputs and declared outputs.
Register the completed routes with your adapter as usual.

## Context And Middleware

Context and middleware are supported in contract-first routes. See [Context](/docs/context) and [Middleware](/docs/middleware) for details.

`$context<T>()` and `.use()` can be called on the root builder to share context and middleware across all routes:

```ts
const implementer = implement(api)
	.$context<{ user: { id: string } }>()
	.use(yourMiddleware);
const createTodo = implementer.todos.create.handler(yourHandler);
```

Alternatively, call them on a specific route to share context and middleware only with that route:

```ts
const implementer = implement(api);
const create = implementer.todos.create
	.$context<{ user: { id: string } }>()
	.use(yourMiddleware)
	.handler(yourHandler);
```

:::error[Reserved Keys]
Don't use `use` or `$context` as top-level keys in your contract. They are reserved for the implementation builder.
:::

## Use in a Client

Pass the shared contract directly to `initClient()` or
`createTanstackQueryUtils()`. No [contract generation](/docs/client/contract-generation)
step is needed, and the client can [validate responses](/docs/client/fetch-client#response-validation)
with the contract's schemas.

See the [Contract-first Quickstart](/docs/contract-first-quickstart) for a
complete example.
