
Company News
Socket Named Top Sales Organization by RepVue
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.
trpc-to-openapi
Advanced tools
Note: This project is a fork of a fork, with full credit to the original authors. It appears that the original author has abandoned the project, so I plan to add new features in the near future.
1. Install trpc-to-openapi.
# pnpm
pnpm add trpc-to-openapi
# npm
npm install trpc-to-openapi
# yarn
yarn add trpc-to-openapi
2. Add OpenApiMeta to your tRPC instance.
import { initTRPC } from '@trpc/server';
import { OpenApiMeta } from 'trpc-to-openapi';
const t = initTRPC.meta<OpenApiMeta>().create(); /* 👈 */
3. Enable openapi support for a procedure.
export const appRouter = t.router({
sayHello: t.procedure
.meta({ /* 👉 */ openapi: { method: 'GET', path: '/say-hello' } })
.input(z.object({ name: z.string() }))
.output(z.object({ greeting: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}!` };
}),
getStatus: t.procedure
.meta({ /* 👉 */ openapi: { method: 'GET', path: '/status' } })
.output(z.object({ status: z.string() }))
.query(() => {
return { status: 'healthy' };
});
});
4. Generate an OpenAPI document.
import { generateOpenApiDocument } from 'trpc-to-openapi';
import { appRouter } from '../appRouter';
import { UserSchema, ProductSchema } from '../schemas';
/* 👇 */
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'tRPC OpenAPI',
version: '1.0.0',
baseUrl: 'http://localhost:3000',
defs: {
UserSchema,
ProductSchema,
},
});
5. Add an trpc-to-openapi handler to your app.
We currently support adapters for Express, Next.js, Fastify, Nuxt & Node:HTTP.
Fetch, Cloudflare Workers & more soon™, PRs are welcomed 🙌.
No support for AWS lambdas
import http from 'http';
import { createOpenApiHttpHandler } from 'trpc-to-openapi';
import { appRouter } from '../appRouter';
const server = http.createServer(createOpenApiHttpHandler({ router: appRouter })); /* 👈 */
server.listen(3000);
6. Profit 🤑
// client.ts
const res = await fetch('http://localhost:3000/say-hello?name=OpenAPI', { method: 'GET' });
const body = await res.json(); /* { greeting: 'Hello OpenAPI!' } */
const statusRes = await fetch('http://localhost:3000/status', { method: 'GET' });
const statusBody = await statusRes.json(); /* { status: 'healthy' } */
Peer dependencies:
For a procedure to support OpenAPI the following must be true:
output parser is present AND uses Zod validation.input parser is present, it must use Zod validation.input parsers (when present) extend Object<{ [string]: String | Number | BigInt | Date }> or Void.input parsers (when present) extend Object<{ [string]: AnyType }> or Void.meta.openapi.method is GET, POST, PATCH, PUT or DELETE.meta.openapi.path is a string starting with /.meta.openapi.path parameters (when present) exist in input parser as String | Number | BigInt | DatePlease note:
transformers (such as superjson) are ignored.Procedures with a GET/DELETE method will accept inputs via URL query parameters. Procedures with a POST/PATCH/PUT method will accept inputs via the request body with a application/json content type.
A procedure can accept a set of inputs via URL path parameters. You can add a path parameter to any OpenAPI procedure by using curly brackets around an input name as a path segment in the meta.openapi.path field.
Query & path parameter inputs are always accepted as a string. This library will attempt to coerce your input values to the following primitive types out of the box: number, boolean, bigint and date. If you wish to support others such as object, array etc. please use z.preprocess().
// Router
export const appRouter = t.router({
sayHello: t.procedure
.meta({ openapi: { method: 'GET', path: '/say-hello/{name}' /* 👈 */ } })
.input(z.object({ name: z.string() /* 👈 */, greeting: z.string() }))
.output(z.object({ greeting: z.string() }))
.query(({ input }) => {
return { greeting: `${input.greeting} ${input.name}!` };
});
});
// Client
const res = await fetch('http://localhost:3000/say-hello/Lily?greeting=Hello' /* 👈 */, {
method: 'GET',
});
const body = await res.json(); /* { greeting: 'Hello Lily!' } */
// Router
export const appRouter = t.router({
sayHello: t.procedure
.meta({ openapi: { method: 'POST', path: '/say-hello/{name}' /* 👈 */ } })
.input(z.object({ name: z.string() /* 👈 */, greeting: z.string() }))
.output(z.object({ greeting: z.string() }))
.mutation(({ input }) => {
return { greeting: `${input.greeting} ${input.name}!` };
});
});
// Client
const res = await fetch('http://localhost:3000/say-hello/Lily' /* 👈 */, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ greeting: 'Hello' }),
});
const body = await res.json(); /* { greeting: 'Hello Lily!' } */
Any custom headers can be specified in the meta.openapi.requestHeaders and meta.openapi.responseHeaders zod object schema, these headers will not be validated. Please consider using Authorization for first-class OpenAPI auth/security support.
Status codes will be 200 by default for any successful requests. In the case of an error, the status code will be derived from the thrown TRPCError or fallback to 500.
You can modify the status code or headers for any response using the responseMeta function.
Please see error status codes here.
To create protected endpoints, add protect: true to the meta.openapi object of each tRPC procedure. By default, you can then authenticate each request with the createContext function using the Authorization header with the Bearer scheme. If you wish to authenticate requests using a different/additional methods (such as custom headers, or cookies) this can be overwritten by specifying securitySchemes object.
Explore a complete example here.
import { TRPCError, initTRPC } from '@trpc/server';
import { OpenApiMeta } from 'trpc-to-openapi';
type User = { id: string; name: string };
const users: User[] = [
{
id: 'usr_123',
name: 'Lily',
},
];
export type Context = { user: User | null };
export const createContext = async ({ req, res }): Promise<Context> => {
let user: User | null = null;
if (req.headers.authorization) {
const userId = req.headers.authorization.split(' ')[1];
user = users.find((_user) => _user.id === userId);
}
return { user };
};
const t = initTRPC.context<Context>().meta<OpenApiMeta>().create();
export const appRouter = t.router({
sayHello: t.procedure
.meta({ openapi: { method: 'GET', path: '/say-hello', protect: true /* 👈 */ } })
.input(z.void()) // no input expected
.output(z.object({ greeting: z.string() }))
.query(({ input, ctx }) => {
if (!ctx.user) {
throw new TRPCError({ message: 'User not found', code: 'UNAUTHORIZED' });
}
return { greeting: `Hello ${ctx.user.name}!` };
}),
});
const res = await fetch('http://localhost:3000/say-hello', {
method: 'GET',
headers: { Authorization: 'Bearer usr_123' } /* 👈 */,
});
const body = await res.json(); /* { greeting: 'Hello Lily!' } */
For advanced use-cases, please find examples in our complete test suite.
Please see full example here.
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import express from 'express';
import { createOpenApiExpressMiddleware } from 'trpc-to-openapi';
import { appRouter } from '../appRouter';
const app = express();
app.use('/api/trpc', createExpressMiddleware({ router: appRouter }));
app.use('/api', createOpenApiExpressMiddleware({ router: appRouter })); /* 👈 */
app.listen(3000);
Please see full example here.
// src/app/[...trpc]/route.ts
import { appRouter } from '~/server/api/root';
import { createContext } from '~/server/api/trpc';
import { type NextRequest } from 'next/server';
import { createOpenApiFetchHandler } from 'trpc-to-openapi';
export const dynamic = 'force-dynamic';
const handler = (req: NextRequest) => {
// Handle incoming OpenAPI requests
return createOpenApiFetchHandler({
endpoint: '/',
router: appRouter,
createContext: () => createContext(req),
req,
});
};
export {
handler as GET,
handler as POST,
handler as PUT,
handler as PATCH,
handler as DELETE,
handler as OPTIONS,
handler as HEAD,
};
Please see full example here.
// pages/api/[...trpc].ts
import { createOpenApiNextHandler } from 'trpc-to-openapi';
import { appRouter } from '../../server/appRouter';
export default createOpenApiNextHandler({ router: appRouter });
Please see full example here.
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import Fastify from 'fastify';
import { fastifyTRPCOpenApiPlugin } from 'trpc-to-openapi';
import { appRouter } from './router';
const fastify = Fastify();
async function main() {
await fastify.register(fastifyTRPCPlugin, { router: appRouter });
await fastify.register(fastifyTRPCOpenApiPlugin, { router: appRouter }); /* 👈 */
await fastify.listen({ port: 3000 });
}
main();
Please see full typings here.
| Property | Type | Description | Required |
|---|---|---|---|
title | string | The title of the API. | true |
description | string | A short description of the API. | false |
version | string | The version of the OpenAPI document. | true |
baseUrl | string | The base URL of the target server. | true |
docsUrl | string | A URL to any external documentation. | false |
tags | string[] | A list for ordering endpoint groups. | false |
securitySchemes | Record<string, SecuritySchemeObject> | Defaults to Authorization header with Bearer scheme | false |
filter | (ctx: { metadata: { openapi: NonNullable<OpenApiMeta['openapi']> } & TMeta }) => boolean | Optional filter function to include/exclude procedures from the generated OpenAPI document. | false |
Please see full typings here.
| Property | Type | Description | Required | Default |
|---|---|---|---|---|
enabled | boolean | Exposes this procedure to trpc-to-openapi adapters and on the OpenAPI document. | false | true |
method | HttpMethod | HTTP method this endpoint is exposed on. Value can be GET, POST, PATCH, PUT or DELETE. | true | undefined |
path | string | Pathname this endpoint is exposed on. Value must start with /, specify path parameters using {}. | true | undefined |
protect | boolean | Requires this endpoint to use a security scheme. | false | true |
summary | string | A short summary of the endpoint included in the OpenAPI document. | false | undefined |
description | string | A verbose description of the endpoint included in the OpenAPI document. | false | undefined |
tags | string[] | A list of tags used for logical grouping of endpoints in the OpenAPI document. | false | undefined |
requestHeaders | AnyZodObject | A zod object schema describing any custom headers to add to the request for this endpoint in the OpenAPI document. | false | undefined |
responseHeaders | AnyZodObject | A zod object schema describing any custom headers to add to the response for this endpoint in the OpenAPI document. | false | undefined |
successDescription | string | A string to use as the description for a successful response. | false | 'Successful response' |
errorResponses | number[] | { [key: number]: string } | A list of error response codes or an object of response codes and their description to add to the responses for this endpoint. | false | undefined |
contentTypes | OpenApiContentType[] | A set of content types specified as accepted in the OpenAPI document. | false | ['application/json'] |
deprecated | boolean | Whether or not to mark an endpoint as deprecated | false | false |
Please see full typings here.
| Property | Type | Description | Required |
|---|---|---|---|
router | Router | Your application tRPC router. | true |
createContext | Function | Passes contextual (ctx) data to procedure resolvers. | false |
responseMeta | Function | Returns any modifications to statusCode & headers. | false |
onError | Function | Called if error occurs inside handler. | false |
maxBodySize | number | Maximum request body size in bytes (default: 100kb). | false |
Distributed under the MIT License. See LICENSE for more information.
Filtering Procedures in OpenAPI Output
You can use the filter option to selectively include or exclude procedures from the generated OpenAPI document. The filter function receives a context object with the procedure's metadata. Return true to include the procedure, or false to exclude it.
Example:
const appRouter = t.router({
publicProc: t.procedure
.meta({ openapi: { method: 'GET', path: '/public' }, isPublic: true })
.input(z.object({}))
.output(z.object({ result: z.string() }))
.query(() => ({ result: 'public' })),
privateProc: t.procedure
.meta({ openapi: { method: 'GET', path: '/private' }, isPublic: false })
.input(z.object({}))
.output(z.object({ result: z.string() }))
.query(() => ({ result: 'private' })),
});
// Only include procedures where isPublic is true
const openApiDocument = generateOpenApiDocument(appRouter, {
...defaultDocOpts,
filter: ({ metadata }) => metadata.isPublic === true,
});
// openApiDocument.paths will only include '/public'
You can also pass a metadata generic to generateOpenApiDocument to get proper types for your custom metadata in the filter function:
// Define your custom metadata type
interface MyMeta {
isPublic: boolean;
}
const openApiDocument = generateOpenApiDocument<MyMeta>(appRouter, {
...defaultDocOpts,
filter: ({ metadata }) => metadata.isPublic === true,
});
FAQs
tRPC OpenAPI
The npm package trpc-to-openapi receives a total of 98,019 weekly downloads. As such, trpc-to-openapi popularity was classified as popular.
We found that trpc-to-openapi demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.

Security News
NIST will stop enriching most CVEs under a new risk-based model, narrowing the NVD's scope as vulnerability submissions continue to surge.

Company News
/Security News
Socket is an initial recipient of OpenAI's Cybersecurity Grant Program, which commits $10M in API credits to defenders securing open source software.