Claude-skill-registry backend-trpc-openapi
Generate OpenAPI/REST endpoints from tRPC routers. Use when you have a tRPC API but need to expose REST endpoints for third-party integrations, mobile apps, or public API documentation. Provides automatic Swagger UI and OpenAPI spec generation. Choose this when you want type-safe internal APIs (tRPC) with REST fallback for external consumers.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/backend-trpc-openapi" ~/.claude/skills/majiayu000-claude-skill-registry-backend-trpc-openapi && rm -rf "$T"
manifest:
skills/data/backend-trpc-openapi/SKILL.mdsource content
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
schema for OpenAPI response types.output() - Use descriptive
andsummarydescription - 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
- https://github.com/mcampa/trpc-to-openapi — Active fork documentation
- https://swagger.io/specification/ — OpenAPI spec
- https://swagger.io/tools/swagger-ui/ — Swagger UI