backend-trpc-openapi

tRPC + OpenAPI Integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "backend-trpc-openapi" with this command: npx skills add petbrains/mvp-builder/petbrains-mvp-builder-backend-trpc-openapi

tRPC + OpenAPI Integration

Overview

Generate REST endpoints and OpenAPI documentation from your tRPC routers. Get the best of both worlds: type-safe internal API with tRPC, REST/Swagger for external consumers.

Package: trpc-to-openapi (active fork of archived trpc-openapi )

Requirements: tRPC v11+, Zod

Key Benefit: Single source of truth — define once in tRPC, expose as both RPC and REST.

When to Use This Skill

✅ Use tRPC + OpenAPI when:

  • Internal apps use tRPC, but need REST for third parties

  • Need Swagger/OpenAPI documentation

  • Mobile apps (non-React Native) need REST endpoints

  • Microservices with mixed languages need interop

  • Public API requires REST standard

❌ Skip OpenAPI layer when:

  • All clients are TypeScript (pure tRPC is better)

  • Internal-only APIs

  • No documentation requirements

Quick Start

Installation

NOTE: trpc-openapi is ARCHIVED, use active fork

npm install trpc-to-openapi swagger-ui-express

npm install -D @types/swagger-ui-express

Setup tRPC with OpenAPI Meta

// src/server/trpc.ts import { initTRPC } from '@trpc/server'; import { OpenApiMeta } from 'trpc-to-openapi';

const t = initTRPC .context<Context>() .meta<OpenApiMeta>() // ← Enable OpenAPI metadata .create();

export const router = t.router; export const publicProcedure = t.procedure;

Define Procedures with OpenAPI Metadata

// src/server/routers/user.ts import { z } from 'zod'; import { router, publicProcedure, protectedProcedure } from '../trpc';

const UserSchema = z.object({ id: z.string(), email: z.string().email(), name: z.string(), });

export const userRouter = router({ // GET /api/users/{id} getById: publicProcedure .meta({ openapi: { method: 'GET', path: '/users/{id}', tags: ['Users'], summary: 'Get user by ID', description: 'Retrieves a single user by their unique identifier', }, }) .input(z.object({ id: z.string() })) .output(UserSchema) .query(async ({ input, ctx }) => { return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }); }),

// GET /api/users?limit=10&cursor=xxx list: publicProcedure .meta({ openapi: { method: 'GET', path: '/users', tags: ['Users'], summary: 'List users', }, }) .input(z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().optional(), })) .output(z.object({ items: z.array(UserSchema), nextCursor: z.string().optional(), })) .query(async ({ input, ctx }) => { // pagination logic }),

// POST /api/users (protected) create: protectedProcedure .meta({ openapi: { method: 'POST', path: '/users', tags: ['Users'], summary: 'Create user', protect: true, // ← Marks as requiring auth in docs }, }) .input(z.object({ email: z.string().email(), name: z.string().min(2), })) .output(UserSchema) .mutation(async ({ input, ctx }) => { return ctx.db.user.create({ data: input }); }),

// PUT /api/users/{id} update: protectedProcedure .meta({ openapi: { method: 'PUT', path: '/users/{id}', tags: ['Users'], protect: true, }, }) .input(z.object({ id: z.string(), name: z.string().optional(), email: z.string().email().optional(), })) .output(UserSchema) .mutation(async ({ input, ctx }) => { const { id, ...data } = input; return ctx.db.user.update({ where: { id }, data }); }),

// DELETE /api/users/{id} delete: protectedProcedure .meta({ openapi: { method: 'DELETE', path: '/users/{id}', tags: ['Users'], protect: true, }, }) .input(z.object({ id: z.string() })) .output(z.object({ success: z.boolean() })) .mutation(async ({ input, ctx }) => { await ctx.db.user.delete({ where: { id: input.id } }); return { success: true }; }), });

Generate OpenAPI Document

// src/server/openapi.ts import { generateOpenApiDocument } from 'trpc-to-openapi'; import { appRouter } from './routers/_app';

export const openApiDocument = generateOpenApiDocument(appRouter, { title: 'My API', version: '1.0.0', baseUrl: process.env.API_URL || 'http://localhost:3000/api', description: 'REST API documentation', securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, });

Serve REST Endpoints + Swagger UI

// src/server/index.ts import express from 'express'; import cors from 'cors'; import swaggerUi from 'swagger-ui-express'; import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { createOpenApiExpressMiddleware } from 'trpc-to-openapi'; import { appRouter } from './routers/_app'; import { createContext } from './context'; import { openApiDocument } from './openapi';

const app = express(); app.use(cors()); app.use(express.json());

// tRPC endpoint (for TypeScript clients) app.use('/trpc', createExpressMiddleware({ router: appRouter, createContext, }));

// REST/OpenAPI endpoints (for external clients) app.use('/api', createOpenApiExpressMiddleware({ router: appRouter, createContext, }));

// Swagger UI documentation app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument));

// OpenAPI JSON spec app.get('/openapi.json', (req, res) => { res.json(openApiDocument); });

app.listen(3000, () => { console.log('Server: http://localhost:3000'); console.log('tRPC: http://localhost:3000/trpc'); console.log('REST: http://localhost:3000/api'); console.log('Docs: http://localhost:3000/docs'); });

URL Parameter Mapping

// Path parameters use {param} syntax .meta({ openapi: { method: 'GET', path: '/users/{id}/posts/{postId}', }, }) .input(z.object({ id: z.string(), // ← Maps to {id} postId: z.string(), // ← Maps to {postId} }))

// Query parameters are auto-mapped for GET .meta({ openapi: { method: 'GET', path: '/users', }, }) .input(z.object({ limit: z.number(), // ← ?limit=10 search: z.string(), // ← &search=foo }))

When to Expose OpenAPI

Scenario Recommendation

Internal TypeScript clients Pure tRPC

Third-party integrations tRPC + OpenAPI

Public API documentation tRPC + OpenAPI

Mobile apps (non-React Native) tRPC + OpenAPI

Microservices (mixed languages) OpenAPI

Rules

Do ✅

  • Add .output() schema for OpenAPI response types

  • Use descriptive summary and description

  • Group related endpoints with tags

  • Mark protected routes with protect: true

  • Use path parameters for resource identifiers

Avoid ❌

  • Exposing all procedures (only add meta to public ones)

  • Missing output schemas (breaks OpenAPI generation)

  • Inconsistent path naming conventions

  • Skipping authentication markers

OpenAPI Metadata Reference

.meta({ openapi: { method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: '/resource/{id}', tags: ['Category'], summary: 'Short description', description: 'Detailed description', protect: boolean, // Requires auth deprecated: boolean, // Mark as deprecated requestHeaders: z.object(), // Custom headers responseHeaders: z.object(), contentTypes: ['application/json'], }, })

Troubleshooting

"OpenAPI generation fails": → Ensure all procedures with meta have .output() → Check Zod schemas are serializable → Verify path parameters match input schema

"REST endpoint returns 404": → Check path matches exactly (case-sensitive) → Verify HTTP method matches → Ensure createOpenApiExpressMiddleware is mounted

"Auth not working on REST": → Check Authorization header format → Verify createContext extracts token → Match auth middleware with tRPC setup

"Swagger UI empty": → Check openApiDocument is generated → Verify /openapi.json returns valid spec → Check console for generation errors

File Structure

src/server/ ├── trpc.ts # tRPC with OpenApiMeta ├── openapi.ts # OpenAPI document generation ├── context.ts # Shared context ├── index.ts # Express server └── routers/ ├── _app.ts # Root router └── user.ts # Procedures with openapi meta

References

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-magic-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-google-fonts

No summary provided by upstream source.

Repository SourceNeeds Review
General

sequential-thinking

No summary provided by upstream source.

Repository SourceNeeds Review
General

figma-design-extraction

No summary provided by upstream source.

Repository SourceNeeds Review