trpc-rest

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-resttrpc-to-openapi
HandlerFetch-native (RequestResponse)Express/Fastify/Fetch adapters
MiddlewarecreateCaller (full chain)Manual procedure invocation
Size4 files, ~1000 LOC~3000 LOC + adapters
Runtime depszod-openapi onlyzod-openapi + adapter deps
tRPC v11YesYes
Zod 4YesYes (recent)

Design principles

  1. Fetch-native onlyRequest in, Response out. No framework adapters.
  2. Full middleware chain — Uses tRPC's createCaller, so auth, validation, and error formatting all run.
  3. Minimal surface — Two functions: createOpenApiFetchHandler and generateOpenApiDocument.
  4. 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',
});

On this page