@dokploy/trpc-openapi

中文文档
0.0.19发布于 2026/04/12MITTypeScriptESM + CJS近 30 天下载 1.5万

tRPC OpenAPI

原始使用文档
文档目录14 节
  1. OpenAPI support for tRPC 🧩
  2. Customizations (Dokploy-style)
  3. Usage
  4. Requirements
  5. HTTP Requests
  6. Path parameters
  7. Query parameters
  8. Request body
  9. Custom headers
  10. HTTP Responses
  11. Authorization
  12. Examples
  13. Types
  14. License

trpc-to-openapi



OpenAPI support for tRPC 🧩

  • Support for tRPC ^11.1.0
  • Easy REST endpoints for your tRPC procedures.
  • Perfect for incremental adoption.
  • Supports all OpenAPI versions.

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.

Customizations (Dokploy-style)

This fork adds the following behaviour (inspired by Dokploy):

  1. Optional input and output parameters are required by default – In the generated OpenAPI document, optional input/output parameters are treated as required.
  2. .meta is optional for all procedures – Procedures without .meta({ openapi: ... }) are still exposed with default OpenAPI metadata (method from procedure type, path from procedure path, enabled: true, protect: true).
  3. Two flags on meta.openapi:
    • override – When true, only the provided openapi object is used (no defaults merged).
    • additional – When true, defaults are applied first, then your openapi values are merged on top.

Usage

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' } */

Requirements

Peer dependencies:

  • tRPC Server v11 (@trpc/server) must be installed.
  • Zod v4 (zod@^4.0.0) must be installed.

For a procedure to support OpenAPI the following must be true:

  • An output parser is present AND uses Zod validation.
  • If an input parser is present, it must use Zod validation.
  • Query input parsers (when present) extend Object<{ [string]: String | Number | BigInt | Date }> or Void.
  • Mutation 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 | Date

Please note:

  • Data transformers (such as superjson) are ignored.
  • Trailing slashes are ignored.
  • Routing is case-insensitive.

HTTP Requests

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.

Path parameters

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 parameters

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!' } */

Request body

// 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!' } */

Custom headers

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.

HTTP Responses

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.

Authorization

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.

Server

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

Client

const res = await fetch('http://localhost:3000/say-hello', {
  method: 'GET',
  headers: { Authorization: 'Bearer usr_123' } /* 👈 */,
});
const body = await res.json(); /* { greeting: 'Hello Lily!' } */

Examples

For advanced use-cases, please find examples in our complete test suite.

With Express

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);

With Next.js app router

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

With Next.js pages router

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

With Fastify

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

Types

GenerateOpenApiDocumentOptions

Please see full typings here.

PropertyTypeDescriptionRequired
titlestringThe title of the API.true
descriptionstringA short description of the API.false
versionstringThe version of the OpenAPI document.true
baseUrlstringThe base URL of the target server.true
docsUrlstringA URL to any external documentation.false
tagsstring[]A list for ordering endpoint groups.false
securitySchemesRecord<string, SecuritySchemeObject>Defaults to Authorization header with Bearer schemefalse
filter(ctx: { metadata: { openapi: NonNullable<OpenApiMeta['openapi']> } & TMeta }) => booleanOptional filter function to include/exclude procedures from the generated OpenAPI document.false

OpenApiMeta

Please see full typings here.

PropertyTypeDescriptionRequiredDefault
enabledbooleanExposes this procedure to trpc-to-openapi adapters and on the OpenAPI document.falsetrue
methodHttpMethodHTTP method this endpoint is exposed on. Value can be GET, POST, PATCH, PUT or DELETE.trueundefined
pathstringPathname this endpoint is exposed on. Value must start with /, specify path parameters using {}.trueundefined
protectbooleanRequires this endpoint to use a security scheme.falsetrue
summarystringA short summary of the endpoint included in the OpenAPI document.falseundefined
descriptionstringA verbose description of the endpoint included in the OpenAPI document.falseundefined
tagsstring[]A list of tags used for logical grouping of endpoints in the OpenAPI document.falseundefined
requestHeadersAnyZodObjectA zod object schema describing any custom headers to add to the request for this endpoint in the OpenAPI document.falseundefined
responseHeadersAnyZodObjectA zod object schema describing any custom headers to add to the response for this endpoint in the OpenAPI document.falseundefined
successDescriptionstringA string to use as the description for a successful response.false'Successful response'
errorResponsesnumber[] | { [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.falseundefined
contentTypesOpenApiContentType[]A set of content types specified as accepted in the OpenAPI document.false['application/json']
deprecatedbooleanWhether or not to mark an endpoint as deprecatedfalsefalse

CreateOpenApiNodeHttpHandlerOptions

Please see full typings here.

PropertyTypeDescriptionRequired
routerRouterYour application tRPC router.true
createContextFunctionPasses contextual (ctx) data to procedure resolvers.false
responseMetaFunctionReturns any modifications to statusCode & headers.false
onErrorFunctionCalled if error occurs inside handler.false
maxBodySizenumberMaximum request body size in bytes (default: 100kb).false

License

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

版本与发布记录

@dokploy/trpc-openapi 共发布 20 个版本,首次发布于 2024/06/24;过去 12 个月发布了 16 个版本。

版本发布日期距今
0.0.19latest2026/04/127 个月前
0.0.182026/03/197 个月前
0.0.172026/02/288 个月前
0.0.162026/02/258 个月前
0.0.152026/02/258 个月前
0.0.142026/02/248 个月前

其他发布通道:beta → 3.1.0-beta.0。

关于 @dokploy/trpc-openapi 的常见问题

@dokploy/trpc-openapi 是什么?

tRPC OpenAPI

如何安装 @dokploy/trpc-openapi?

在项目目录运行 npm install @dokploy/trpc-openapi,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@trpc/server(^11.1.0)、zod(^4.3.6)、zod-openapi(^5.0.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@dokploy/trpc-openapi 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dokploy/trpc-openapi 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@dokploy/trpc-openapi 可以商用吗?

@dokploy/trpc-openapi 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dokploy/trpc-openapi 的下载量有多少?

根据 npm 官方下载统计,@dokploy/trpc-openapi 在 2026/10/13 – 2026/11/11 这 30 天内被下载 15,273 次,日均约 509 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dokploy/trpc-openapi 还在维护吗?

最新版本 0.0.19 发布于 2026/04/12(7 个月前);至今共发布 20 个版本,其中过去 12 个月发布了 16 个;首次发布于 2024/06/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。