Server
Understand the server-side differences when routes and handlers are defined together.
This page only covers what changes from contract-first handlers. Request schemas, validation, encodings, and adapter integration otherwise work as described in HTTP Requests, Node HTTP, and Fetch Runtime.
Define The Handler On The Route
Import route from the Node or Fetch adapter and end the route chain with
.handler() instead of passing a contract route to implement().
import { route } from "@rest-rpc/node";
import { z } from "zod";
const getTodo = route
.get("/todos/:id")
.params(z.object({ id: z.string() }))
.handler(({ id }) => {
const todo = findTodo(id);
return todo
? { status: 200 as const, body: todo }
: {
status: 404 as const,
body: { code: "TODO_NOT_FOUND" },
};
});
Infer Responses From The Handler
Without .response() declarations, the handler return type defines the route’s
response union:
| Handler return | Inferred response |
|---|---|
{ status, responseHeaders } |
Empty response + response headers |
{ status, body } |
JSON response |
{ status, body: AsyncIterable } |
NDJSON stream |
{ status, contentType, body } |
Custom response |
{ status, contentType, body: AsyncIterable } |
Custom response stream |
sseEvent(data, options) |
SSE event stream |
Each returned literal status becomes a member of the client-visible response union.
Declare Responses When Needed
Response declarations remain available. Adding .response() makes those
declarations the source of the response type and enables the same response
schemas, headers, validation, and handler return forms documented in
HTTP Responses.
const getTodo = route
.get("/todos/:id")
.params(z.object({ id: z.string() }))
.response(200, todoSchema)
.response(404, notFoundSchema)
.handler(({ id }) => {
const todo = findTodo(id);
return todo
? { status: 200, body: todo }
: { status: 404, body: { code: "TODO_NOT_FOUND" } };
});