trpc-openapi
trpc-openapi copied to clipboard
OpenAPI support for tRPC π§©

OpenAPI support for tRPC π§©
- Easy REST endpoints for your tRPC procedures.
- Perfect for incremental adoption.
- OpenAPI version 3.0.3.
Usage
1. Install trpc-openapi.
# npm
npm install trpc-openapi
# yarn
yarn add trpc-openapi
2. Add OpenApiMeta to your tRPC router.
import * as trpc from '@trpc/server';
import { OpenApiMeta } from 'trpc-openapi';
export const appRouter = trpc.router<any, OpenApiMeta /* π */>();
3. Enable openapi support for a procedure.
import * as trpc from '@trpc/server';
import { OpenApiMeta } from 'trpc-openapi';
export const appRouter = trpc.router<any, OpenApiMeta>().query('sayHello', {
meta: { /* π */ openapi: { enabled: true, method: 'GET', path: '/say-hello' } },
input: z.object({ name: z.string() }),
output: z.object({ greeting: z.string() }),
resolve: ({ input }) => {
return { greeting: `Hello ${input.name}!` };
},
});
4. Generate OpenAPI v3 document.
import { generateOpenApiDocument } from 'trpc-openapi';
import { appRouter } from '../appRouter';
/* π */
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'tRPC OpenAPI',
version: '1.0.0',
baseUrl: 'http://localhost:3000',
});
5. Add an trpc-openapi handler to your app.
We currently support adapters for Express, Next.js & node:http.
Fastify & Serverless soonβ’, PRs are welcomed π.
import http from 'http';
import { createOpenApiHttpHandler } from 'trpc-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=James', { method: 'GET' });
const body = await res.json(); /* { ok: true, data: { greeting: 'Hello James!' } } */
Requirements
Peer dependencies:
For a procedure to support OpenAPI the following must be true:
- Both
inputandoutputparsers are present AND useZodvalidation. - Query
inputparsers extendZodObject<{ [string]: ZodString }>orZodVoid. - Mutation
inputparsers extendZodObject<{ [string]: ZodAnyType }>orZodVoid. meta.openapi.enabledis set totrue.meta.openapi.methodisGET,POST,PATCH,PUTorDELETE.meta.openapi.pathis a string starting with/.meta.openapi.pathparameters exist ininputparser asZodString
Please note:
- Data
transformersare 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 enabled 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, if you wish to support other primitives such as number, boolean, Date etc. please use z.preprocess().
// Router
export const appRouter = trpc.router<Context, OpenApiMeta>().query('sayHello', {
meta: { openapi: { enabled: true, method: 'GET', path: '/say-hello/{name}' /* π */ } },
input: z.object({ name: z.string() /* π */, greeting: z.string() }),
output: z.object({ greeting: z.string() }),
resolve: ({ input }) => {
return { greeting: `${input.greeting} ${input.name}!` };
},
});
// Client
const res = await fetch('http://localhost:3000/say-hello/James?greeting=Hello' /* π */, {
method: 'GET',
});
const body = await res.json(); /* { ok: true, data: { greeting: 'Hello James!' } } */
Request body
// Router
export const appRouter = trpc.router<Context, OpenApiMeta>().mutation('sayHello', {
meta: { openapi: { enabled: true, method: 'POST', path: '/say-hello/{name}' /* π */ } },
input: z.object({ name: z.string() /* π */, greeting: z.string() }),
output: z.object({ greeting: z.string() }),
resolve: ({ input }) => {
return { greeting: `${input.greeting} ${input.name}!` };
},
});
// Client
const res = await fetch('http://localhost:3000/say-hello/James' /* π */, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ greeting: 'Hello' }),
});
const body = await res.json(); /* { ok: true, data: { greeting: 'Hello James!' } } */
Custom headers
Any custom headers can be specified in the meta.openapi.headers array, these headers will not be validated on request. Please consider using Authorization for first-class OpenAPI auth/security support.
HTTP Responses
Inspired by Slack Web API.
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.
{
"ok": true,
"data": "This is good" /* Output from tRPC procedure */
}
{
"ok": false,
"error": {
"message": "This is bad", /* Message from TRPCError */,
"code": "BAD_REQUEST", /* Code from TRPCError */
"issues": [...] /* (optional) ZodIssues[] from TRPCError */
}
}
Authorization
To create protected endpoints, add protect: true to the meta.openapi object of each tRPC procedure. You can then authenticate each request with the createContext function using the Authorization header with the Bearer scheme.
Explore a complete example here.
Server
import * as trpc from '@trpc/server';
import { OpenApiMeta } from 'trpc-openapi';
type User = { id: string; name: string };
const users: User[] = [
{
id: 'usr_123',
name: 'James',
},
];
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 };
};
export const appRouter = trpc.router<Context, OpenApiMeta>().query('sayHello', {
meta: { openapi: { enabled: true, method: 'GET', path: '/say-hello', protect: true /* π */ } },
input: z.void(), // no input expected
output: z.object({ greeting: z.string() }),
resolve: ({ input, ctx }) => {
if (!ctx.user) {
throw new trpc.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(); /* { ok: true, data: { greeting: 'Hello James!' } } */
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-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
Please see full example here.
// pages/api/[trpc].ts
import { createOpenApiNextHandler } from 'trpc-openapi';
import { appRouter } from '../../server/appRouter';
export default createOpenApiNextHandler({ router: appRouter });
Types
GenerateOpenApiDocumentOptions
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 |
OpenApiMeta
Please see full typings here.
| Property | Type | Description | Required | Default |
|---|---|---|---|---|
enabled |
boolean |
Exposes this procedure to trpc-openapi adapters and on the OpenAPI document. |
true |
false |
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 an Authorization header credential with Bearer scheme on OpenAPI document. |
false |
false |
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 |
headers |
ParameterObject[] |
An array of custom headers to add for this endpoint in the OpenAPI document. | false |
undefined |
CreateOpenApiNodeHttpHandlerOptions
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 |
teardown |
Function |
Called after each request is completed. | false |
maxBodySize |
number |
Maximum request body size in bytes (default: 100kb). | false |
License
Distributed under the MIT License. See LICENSE for more information.
Contact
James Berry - Follow me on Twitter @jlalmes π