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