Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

Fetch Runtime

Use rest-rpc with Fetch runtimes

Install

pnpm add @rest-rpc/fetch

Usage

import { createRouteHandler, router } from "@rest-rpc/fetch";
import { api } from "./contract";

declare module "@rest-rpc/fetch" {
	interface DefaultRuntimeContext {
		env: Env;
		ctx: ExecutionContext;
	}
	interface DefaultRequest extends NextRequest {}
}

const routes = router(api)
	.middleware(({ runtime }) => ({
		db: runtime.env.DB,
	}))
	.handlers({
		todos: {
			list({ context }) {
				return listTodos(context.db);
			},
			get({ id, context }) {
				return getTodo(context.db, id);
			},
			create({ title, context }) {
				return createTodo(context.db, { title });
			},
		},
	});

const handleRequest = createRouteHandler(routes);

export default {
	fetch(request, env, ctx) {
		return handleRequest(request, { env, ctx });
	},
};

Middleware

type MiddlewareInput<Runtime> = {
	context: Record<string, unknown>;
	request: Request;
	route: HttpRouteDeclaration;
	runtime: Runtime;
};

Augment DefaultRuntimeContext to type the runtime context passed to handleRequest. That runtime is available to middleware. Route handlers receive the context returned by middleware with request added.

You can also augment DefaultRequest if your runtime extends the default Request type.

Stacking Middleware

Stacking middleware is supported. Middleware can add to the context, and the context is passed to the next middleware in the stack. The final context is passed to the route handler.

const routes = router(api)
	.middleware(({ runtime }) => ({
		db: runtime.env.DB,
	}))
	.middleware(({ context }) => ({
		todos: createTodoService(context.db),
	}))
	.handlers({
		todos: {
			list({ context }) {
				return context.todos.list();
			},
		},
	});

Nested Routers

const adminRoutes = router(api.admin)
	.middleware(({ runtime }) => ({
		adminDb: runtime.env.ADMIN_DB,
	}))
	.handlers({
		users: {
			list({ context }) {
				return listAdminUsers(context.adminDb);
			},
		},
	});

const routes = router(api)
	.middleware(({ runtime }) => ({
		db: runtime.env.DB,
	}))
	.handlers({
		admin: adminRoutes, // uses adminDb, not db
		health({ context }) {
			return checkHealth(context.db);
		},
	});

Options

type CreateFetchHandlerOptions = {
	errorHandlers?: ServerErrorHandlers<Record<never, never>>;
	parseBody?: FetchRouteParseBody;
};

Error Handlers

Request validation errors use onRequestValidationError. Response contract validation errors use onResponseValidationError and default to a generic 500 response. Other unhandled route errors use onUnhandledError, or are re-thrown when that hook is omitted or returns undefined.

const handleRequest = createRouteHandler(routes, {
	errorHandlers: {
		onRequestValidationError: ({ issues }) => ({
			status: 422,
			body: { code: "VALIDATION_ERROR", issues },
		}),
		onResponseValidationError: () => ({
			status: 500,
			body: { code: "INVALID_RESPONSE" },
		}),
		onUnhandledError: () => ({
			status: 500,
			body: { code: "INTERNAL_SERVER_ERROR" },
		}),
	},
});

Body Parsing

const handleUpload = createRouteHandler(routes, {
	parseBody: ({ request }) => request.formData(),
});

Was this page helpful?