jqhoogland / trpc-openapi

OpenAPI support for tRPC 🧩

Home Page:https://www.npmjs.com/package/trpc-openapi

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

trpc-openapi

trpc-openapi



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:

  • tRPC Server v9 (@trpc/server@^9.23.0) must be installed.
  • Zod v3 (zod@^3.14.4) must be installed.

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

  • Both input and output parsers are present AND use Zod validation.
  • Query input parsers extend ZodObject<{ [string]: ZodString }> or ZodVoid.
  • Mutation input parsers extend ZodObject<{ [string]: ZodAnyType }> or ZodVoid.
  • meta.openapi.enabled is set to true.
  • meta.openapi.method is GET, DELETE for query OR POST, PUT or PATCH for mutation.
  • meta.openapi.path is a string starting with /.
  • meta.openapi.path parameters exist in input parser as ZodString

Please note:

  • Data transformers are ignored.
  • Trailing slashes are ignored.
  • Routing is case-insensitive.

HTTP Requests

Query procedures accept input via URL query parameters.

Mutation procedures accept input via the request body with a application/json content type.

Path parameters

Both queries & mutations can accept a set of their 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

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

Mutation

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

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" /* tRPC procedure output */
}
{
  "ok": false,
  "error": {
    "message": "This is bad" /* Message from TRPCError */,
    "code": "BAD_REQUEST" /* Code 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 Method this endpoint is exposed on. Value can be GET/DELETE for queries OR POST/PUT/PATCH for mutations. 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

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 πŸ’š

About

OpenAPI support for tRPC 🧩

https://www.npmjs.com/package/trpc-openapi

License:MIT License


Languages

Language:TypeScript 99.2%Language:JavaScript 0.8%