Hanzo
Hanzo Skills Reference

Hanzo tRPC OpenAPI

`@hanzo/trpc-openapi` generates OpenAPI 3.x documents from tRPC v11 routers and serves those procedures as REST through adapters for Express, Fastify, Next.js, Nuxt, Koa and the Fetch API. One router definition, two surfaces, no drift.

Overview

@hanzo/trpc-openapi is a TypeScript library that generates OpenAPI 3.x documents from tRPC v11 routers and provides HTTP adapters to serve tRPC procedures as REST endpoints. Published on npm as @hanzo/trpc-openapi. One router definition produces both the tRPC client types and the REST surface, so the OpenAPI document can never drift from the code that serves it.

Why @hanzo/trpc-openapi?

  • tRPC + REST: Expose tRPC procedures as standard REST endpoints without duplicating code
  • OpenAPI doc generation: Auto-generate Swagger/OpenAPI specs from your tRPC router definitions
  • Multiple adapters: Express, Fastify, Next.js (pages + app router), Nuxt, Koa, Fetch API, standalone Node HTTP
  • Zod v4 native: Built for Zod v4 with zod-openapi v5 integration
  • Optional meta: Procedures without .meta({ openapi: ... }) are still exposed with sensible defaults (method from procedure type, path from procedure path)

Tech Stack

  • Language: TypeScript 5.8
  • Runtime: Node.js
  • Package Manager: pnpm
  • tRPC: v11.1.0+ (peer dependency)
  • Zod: v4.x (peer dependency)
  • OpenAPI: openapi3-ts v4.4.0, zod-openapi v5.x (peer dependency)
  • Testing: Jest 29 + ts-jest
  • Linting: ESLint + Prettier
  • Build: TypeScript compiler (dual CJS + ESM output)

Behaviour worth knowing

  • Optional input/output parameters are emitted as required in the generated document, so consumers get a tight contract rather than an all-optional blob
  • .meta({ openapi: ... }) is optional on every procedure — without it the method comes from the procedure type and the path from the procedure path
  • override flag: use only the provided openapi object, merge no defaults
  • additional flag: merge the provided openapi on top of the defaults

Repo: github.com/hanzoai/trpc-openapi

When to use

  • Exposing tRPC procedures as REST/OpenAPI endpoints
  • Generating OpenAPI/Swagger documentation from tRPC routers
  • Building APIs that need both tRPC and REST access
  • Incremental migration between REST and tRPC
  • Auto-documenting Hanzo platform tRPC services

Hard requirements

  1. @trpc/server ^11.1.0
  2. zod ^4.0.0 (Zod v4, not v3)
  3. zod-openapi ^5.0.1
  4. Node.js 22+ (based on @types/node ^22)

Quick reference

ItemValue
npm Package@hanzo/trpc-openapi
Version0.0.17
Repogithub.com/hanzoai/trpc-openapi
Branchmain
LicenseMIT
tRPC Version^11.1.0 (peer dep)
Zod Version^4.0.0 (peer dep)
Buildpnpm build (runs tests first)
Testpnpm test
ExportsCJS (dist/cjs/) + ESM (dist/esm/)

One-file quickstart

Install

pnpm add @hanzo/trpc-openapi
# peer deps
pnpm add @trpc/server zod zod-openapi

Define a tRPC router with OpenAPI metadata

import { initTRPC } from '@trpc/server';
import { z } from 'zod';
import { OpenApiMeta, generateOpenApiDocument, createOpenApiExpressMiddleware } from '@hanzo/trpc-openapi';
import express from 'express';

// 1. Create tRPC instance with OpenApiMeta
const t = initTRPC.meta<OpenApiMeta>().create();

// 2. Define router with openapi metadata
const appRouter = t.router({
  getUser: t.procedure
    .meta({ openapi: { method: 'GET', path: '/users/{id}' } })
    .input(z.object({ id: z.string() }))
    .output(z.object({ id: z.string(), name: z.string() }))
    .query(({ input }) => ({ id: input.id, name: 'Hanzo User' })),

  createUser: t.procedure
    .meta({ openapi: { method: 'POST', path: '/users', protect: true } })
    .input(z.object({ name: z.string(), email: z.string().email() }))
    .output(z.object({ id: z.string(), name: z.string() }))
    .mutation(({ input }) => ({ id: 'usr_new', name: input.name })),
});

// 3. Generate OpenAPI document
const openApiDoc = generateOpenApiDocument(appRouter, {
  title: 'My API',
  version: '1.0.0',
  baseUrl: 'http://localhost:3000',
});

// 4. Serve REST endpoints
const app = express();
app.get('/openapi.json', (req, res) => res.json(openApiDoc));
app.use('/api', createOpenApiExpressMiddleware({ router: appRouter }));
app.listen(3000);

Core Concepts

Architecture

┌──────────────────────┐
│  tRPC Router         │
│  (procedures + meta) │
└──────┬───────────────┘

  ┌────▼────────────────────┐
  │  generateOpenApiDocument │──── OpenAPI 3.x JSON
  └────┬────────────────────┘

  ┌────▼────────────────────┐
  │  HTTP Adapter            │──── REST endpoints
  │  (Express/Fastify/Next)  │     GET /users/{id}
  └─────────────────────────┘     POST /users

Adapters

AdapterImportHandler Factory
Express@hanzo/trpc-openapicreateOpenApiExpressMiddleware
Fastify@hanzo/trpc-openapifastifyTRPCOpenApiPlugin
Next.js Pages@hanzo/trpc-openapicreateOpenApiNextHandler
Next.js App Router@hanzo/trpc-openapicreateOpenApiFetchHandler
Nuxt@hanzo/trpc-openapicreateOpenApiNuxtHandler
Koa@hanzo/trpc-openapicreateOpenApiKoaMiddleware
Fetch@hanzo/trpc-openapicreateOpenApiFetchHandler
Node HTTP@hanzo/trpc-openapicreateOpenApiHttpHandler
Standalone@hanzo/trpc-openapicreateOpenApiHttpHandler (with http.createServer)

OpenApiMeta Fields

FieldTypeDefaultDescription
methodGET|POST|PATCH|PUT|DELETE(from proc type)HTTP method
path/${string}(from proc path)URL path with {param} placeholders
enabledbooleantrueInclude in OpenAPI doc and adapters
protectbooleantrueRequire Authorization header
overridebooleanfalseUse only provided openapi object (no defaults)
additionalbooleanfalseMerge provided openapi on top of defaults
tagsstring[]--Grouping tags
summarystring--Short description
descriptionstring--Verbose description
deprecatedbooleanfalseMark as deprecated
contentTypesstring[]['application/json']Accepted content types
requestHeadersZodObject--Custom request headers schema
responseHeadersZodObject--Custom response headers schema
errorResponsesnumber[]|Record<number,string>--Error response codes

Input Handling

  • GET/DELETE: Input from URL query parameters (auto-coerced to number, boolean, bigint, date)
  • POST/PATCH/PUT: Input from JSON request body
  • Path parameters: Defined as {paramName} in the path, must exist in input schema as string, number, bigint, or date

Filtering Procedures

const doc = generateOpenApiDocument<{ isPublic: boolean }>(appRouter, {
  title: 'API',
  version: '1.0.0',
  baseUrl: 'http://localhost:3000',
  filter: ({ metadata }) => metadata.isPublic === true,
});

Directory structure

trpc-openapi/
├── src/
│   ├── index.ts              # Re-exports everything
│   ├── types.ts              # OpenApiMeta, OpenApiMethod, response types
│   ├── generator/
│   │   ├── index.ts          # generateOpenApiDocument()
│   │   ├── paths.ts          # OpenAPI path generation
│   │   └── schema.ts         # Zod-to-OpenAPI schema conversion
│   ├── adapters/
│   │   ├── express.ts        # Express middleware
│   │   ├── fastify.ts        # Fastify plugin
│   │   ├── next.ts           # Next.js handler (pages + app)
│   │   ├── nuxt.ts           # Nuxt handler
│   │   ├── koa.ts            # Koa middleware
│   │   ├── fetch.ts          # Fetch API handler
│   │   ├── standalone.ts     # Standalone Node HTTP
│   │   └── node-http/        # Core Node HTTP handler
│   └── utils/                # Internal utilities
├── test/
│   ├── generator.test.ts     # 115KB of generator tests
│   └── adapters/             # Adapter-specific tests
├── examples/
│   ├── with-express/
│   ├── with-fastify/
│   ├── with-nextjs/
│   ├── with-nextjs-appdir/
│   └── with-nuxtjs/
├── dist/
│   ├── cjs/                  # CommonJS build output
│   └── esm/                  # ESM build output
├── package.json              # @hanzo/trpc-openapi v0.0.17
├── tsconfig.json
└── jest.config.ts

Troubleshooting

IssueCauseSolution
zod is not a functionUsing Zod v3 instead of v4Upgrade to zod ^4.0.0
Procedures missing from OpenAPI docenabled: false in metaRemove or set enabled: true
Query params not parsedComplex types in GET inputUse z.preprocess() for objects/arrays in query params
404 on REST endpointPath mismatchEnsure meta.openapi.path matches exactly, paths are case-insensitive
Auth not workingprotect: true but no createContextImplement createContext to extract Authorization header
Type error with metatRPC not configured with OpenApiMetaUse initTRPC.meta<OpenApiMeta>().create()
  • hanzo/hanzo-platform.md - PaaS platform (uses tRPC + this library)
  • hanzo/hanzo-openapi.md - OpenAPI specification patterns
  • hanzo/hanzo-ai-api.md - the Hanzo AI API

How is this guide?

On this page