Introduction
OpenAPI 3.1 spec generation and Fetch-native REST handler for tRPC v11 + Zod 4.
Generate OpenAPI 3.1.0 specs and handle REST requests from tRPC routers. Built for tRPC v11 and Zod 4.
Why this exists
We ran trpc-to-openapi in production for 8 months. We hit:
- Error response caching bug — error messages were shared across unrelated endpoints by status code
- Bodyless POST 415 error — the library rejected POST endpoints with path-only params (no body)
- Lost query param descriptions —
.optional()silently stripped OpenAPI metadata
When Zod 4 landed, we built what we needed instead of patching again.
How it's different
| trpc-rest | trpc-to-openapi | |
|---|---|---|
| Handler | Fetch-native (Request → Response) | Express/Fastify/Fetch adapters |
| Middleware | createCaller (full chain) | Manual procedure invocation |
| Size | 4 files, ~1000 LOC | ~3000 LOC + adapters |
| Runtime deps | zod-openapi only | zod-openapi + adapter deps |
| tRPC v11 | Yes | Yes |
| Zod 4 | Yes | Yes (recent) |
Design principles
- Fetch-native only —
Requestin,Responseout. No framework adapters. - Full middleware chain — Uses tRPC's
createCaller, so auth, validation, and error formatting all run. - Minimal surface — Two functions:
createOpenApiFetchHandlerandgenerateOpenApiDocument. - No schema mutation — Query param coercion creates new objects, never modifies your Zod schemas.
Quick example
import { createOpenApiFetchHandler, generateOpenApiDocument } from 'trpc-rest';
// Handle REST requests
const response = await createOpenApiFetchHandler({
router: appRouter,
endpoint: '/api',
req: request,
createContext: ({ req }) => createContext(req),
});
// Generate OpenAPI spec
const spec = generateOpenApiDocument(appRouter, {
title: 'My API',
version: '1.0.0',
baseUrl: 'https://api.example.com',
});