---
title: Quickstart
description: Define HTTP routes with handlers and call them through a typed client.
---

`rest-rpc` builds REST APIs with RPC-like ergonomics. Start with default
methods and paths, or declare request segments and response statuses explicitly.
Both forms are ordinary HTTP routes.

This guide uses Express and Zod to build two routes: a minimal create operation
and an explicit GET with path params, query input, and success/error responses.
Their output types are inferred from the handlers.

## Install

```package-install
npm i @rest-rpc/core @rest-rpc/express express zod
```

Use an existing TypeScript project with a `tsconfig.json`. For other frameworks
and runtimes, see the [server adapters](/docs/server/express).

## Define and Register Routes

```ts server.ts
import express from "express";
import { route, registerRoutes } from "@rest-rpc/express";
import { z } from "zod";

type Todo = { id: string; title: string; completed: boolean };
const todos = new Map<string, Todo>();

const create = route
	.input(z.object({ title: z.string().min(1) }))
	.handler(({ input: { title } }) => {
		const todo: Todo = {
			id: crypto.randomUUID(),
			title,
			completed: false,
		};
		todos.set(todo.id, todo);
		return todo;
	});

const get = route
	.get("/todos/:id")
	.params(z.object({ id: z.string() }))
	.query(z.object({ uppercase: z.enum(["yes", "no"]).optional() }))
	.handler(({ params, query }) => {
		const todo = todos.get(params.id);
		if (!todo) {
			return { status: 404, body: { code: "TODO_NOT_FOUND" as const } };
		}
		return {
			status: 200,
			body: {
				...todo,
				title:
					query.uppercase === "yes" ? todo.title.toUpperCase() : todo.title,
			},
		};
	});

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

const app = express();
app.use(express.json());
registerRoutes(app, routes);
app.listen(3000);
```

`todos.create` uses `POST /todos/create`, sends its flat input as JSON, and
returns a plain result with status 200. `todos.get` uses
`GET /todos/:id?uppercase=yes` and returns a status envelope. The client keeps
these same input and result shapes.

The request schemas validate inputs, and the handler's return value determines
the client's result type.

## Create the Client

Derive the client definition from the exported routes:

```ts client-contract.ts
import { generateContractFromType } from "@rest-rpc/core/generate";
import type { routes } from "./server";

export const api = generateContractFromType<typeof routes>();
```

For browser clients, run generation at build time. See
[Client Contract Generation](/docs/client/contract-generation) for setup.

## Call the Routes

```ts client.ts
import { initClient } from "@rest-rpc/core";
import { api } from "./client-contract";

const client = initClient(api, { baseUrl: "http://localhost:3000" });

const todo = await client.todos.create({ title: "Write docs" });
const response = await client.todos.get({
	params: { id: todo.id },
	query: { uppercase: "yes" },
});

if (response.status === 200) {
	console.log(response.body.title);
} else {
	console.log(response.body.code); // "TODO_NOT_FOUND"
}
```

The calls are typed functions, while the requests still use the methods, URLs,
query parameters, and statuses you defined. Other HTTP clients can call the same
endpoints.

## Next Steps

- [Route Builder](/docs/route-builder) covers explicit outputs, content types, streams, and metadata.
- [Fetch Client](/docs/client/fetch-client) covers client behavior and options.
- [TanStack Query](/docs/client/tanstack-query) adds query and mutation utils.
- [Contract-first Quickstart](/docs/contract-first-quickstart) builds the same API from a separately shared contract.
