@@ -15,7 +15,11 @@ static type extraction via the TypeScript Compiler API.
1515- ** Output type visualization** — automatic runtime detection from ` .output() `
1616 schemas, or static extraction via the CLI using the TypeScript Compiler API
1717- ** Sidebar navigation** — procedures grouped by router, color-coded badges
18- (query/mutation/subscription), real-time search
18+ (query/mutation/subscription), real-time search, tag filtering
19+ - ** Authentication** — configurable "Authorize" button (bearer, cookie, header,
20+ basic) with localStorage persistence
21+ - ** Procedure metadata** — automatically displays ` .meta() ` values (auth,
22+ deprecated, tags, custom fields)
1923- ** Self-contained** — served as a single HTML response, no static file hosting
2024 needed
2125- ** tRPC v10 & v11** compatible
@@ -115,18 +119,26 @@ npx @srawad/trpc-studio extract --router ./src/server/router.ts
115119```
116120
117121This analyzes your TypeScript source and generates a ` .trpc-studio.json ` file
118- with JSON Schema definitions for every procedure's output type.
122+ with JSON Schema definitions for every procedure's input and output types.
123+
124+ This is especially useful for:
125+
126+ - ** Output types** — procedures that don't use ` .output() ` (most tRPC code)
127+ - ** Input types with ` z.custom<T>() ` ** — Zod's ` z.custom() ` carries no runtime
128+ schema, so the UI renders it as an empty object. The CLI extractor resolves
129+ the actual TypeScript type and provides full structural info.
119130
120131#### Step 2: Pass the schemas to renderTrpcStudio
121132
122133``` typescript
123- import outputSchemas from " ./.trpc-studio.json" ;
134+ import schemas from " ./.trpc-studio.json" ;
124135
125136app .get (" /studio" , (_req , res ) => {
126137 res .send (
127138 renderTrpcStudio (appRouter , {
128139 url: " http://localhost:3000/trpc" ,
129- outputSchemas ,
140+ inputSchemas: schemas .inputs ,
141+ outputSchemas: schemas .outputs ,
130142 }),
131143 );
132144});
@@ -176,49 +188,75 @@ npx @srawad/trpc-studio extract \
176188 --out .trpc-studio.json
177189```
178190
179- This generates ` .trpc-studio.json ` :
191+ This generates ` .trpc-studio.json ` with both input and output schemas :
180192
181193``` json
182194{
183- "hello" : {
184- "type" : " object" ,
185- "properties" : {
186- "greeting" : { "type" : " string" }
195+ "inputs" : {
196+ "hello" : {
197+ "type" : " object" ,
198+ "properties" : {
199+ "name" : { "type" : " string" }
200+ }
187201 },
188- "required" : [" greeting" ]
189- },
190- "user.getById" : {
191- "type" : " object" ,
192- "properties" : {
193- "id" : { "type" : " string" },
194- "name" : { "type" : " string" },
195- "email" : { "type" : " string" }
202+ "user.getById" : {
203+ "type" : " object" ,
204+ "properties" : {
205+ "id" : { "type" : " string" }
206+ },
207+ "required" : [" id" ]
196208 },
197- "required" : [" id" , " name" , " email" ]
209+ "user.create" : {
210+ "type" : " object" ,
211+ "properties" : {
212+ "name" : { "type" : " string" },
213+ "email" : { "type" : " string" }
214+ },
215+ "required" : [" name" , " email" ]
216+ }
198217 },
199- "user.create " : {
200- "type " : " object " ,
201- "properties " : {
202- "id " : { "type" : " string " },
203- "name " : { "type" : " string" },
204- "email" : { "type" : " string " },
205- "createdAt " : { "type" : " string " }
218+ "outputs " : {
219+ "hello " : {
220+ "type " : " object " ,
221+ "properties " : {
222+ "greeting " : { "type" : " string" }
223+ },
224+ "required " : [ " greeting " ]
206225 },
207- "required" : [" id" , " name" , " email" , " createdAt" ]
226+ "user.getById" : {
227+ "type" : " object" ,
228+ "properties" : {
229+ "id" : { "type" : " string" },
230+ "name" : { "type" : " string" },
231+ "email" : { "type" : " string" }
232+ },
233+ "required" : [" id" , " name" , " email" ]
234+ },
235+ "user.create" : {
236+ "type" : " object" ,
237+ "properties" : {
238+ "id" : { "type" : " string" },
239+ "name" : { "type" : " string" },
240+ "email" : { "type" : " string" },
241+ "createdAt" : { "type" : " string" }
242+ },
243+ "required" : [" id" , " name" , " email" , " createdAt" ]
244+ }
208245 }
209246}
210247```
211248
212249Then wire it up:
213250
214251``` typescript
215- import outputSchemas from " ./.trpc-studio.json" ;
252+ import schemas from " ./.trpc-studio.json" ;
216253
217254app .get (" /studio" , (_req , res ) => {
218255 res .send (
219256 renderTrpcStudio (appRouter , {
220257 url: " http://localhost:3000/trpc" ,
221- outputSchemas ,
258+ inputSchemas: schemas .inputs ,
259+ outputSchemas: schemas .outputs ,
222260 meta: { title: " My API" , version: " 1.0.0" },
223261 }),
224262 );
@@ -247,6 +285,88 @@ app.get("/studio", (_req, res) => {
247285| `--name <name>` | `appRouter` | Name of the exported router variable |
248286| `--help` | | Show help message |
249287
288+ ## Procedure Metadata (.meta())
289+
290+ trpc-studio automatically reads and displays tRPC's built-in `.meta()` on each
291+ procedure. No configuration needed — if your procedures have meta, it shows up.
292+
293+ ```typescript
294+ const t = initTRPC
295+ .meta<{
296+ auth?: boolean;
297+ deprecated?: boolean;
298+ rateLimit?: number;
299+ tags?: string[];
300+ }>()
301+ .create();
302+
303+ const protectedProcedure = t.procedure.meta({ auth: true }).use(authMiddleware);
304+
305+ export const appRouter = t.router({
306+ getUser: protectedProcedure
307+ .meta({ tags: ["billing", "v2"] })
308+ .input(z.object({ id: z.string() }))
309+ .query(/* ... */),
310+
311+ legacyReport: t.procedure
312+ .meta({ deprecated: true, auth: true, tags: ["reporting"] })
313+ .query(/* ... */),
314+ });
315+ ```
316+
317+ ** In the UI:**
318+
319+ - ** Sidebar** — 🔒 icon for ` auth: true ` , ⚠️ icon for ` deprecated: true ` , tag
320+ badges next to procedure names
321+ - ** Detail panel** — all meta keys rendered as badge pills below the procedure
322+ header (e.g., ` 🔒 auth ` , ` ⚠️ deprecated ` , ` rateLimit: 100 ` )
323+ - ** Tag filtering** — if any procedures use ` meta.tags ` , a filter bar appears at
324+ the top of the sidebar. Click tags to filter procedures across all routers.
325+ Multi-select supported.
326+
327+ The rendering is generic — trpc-studio doesn't interpret the meta values, it
328+ just displays whatever keys and values are present.
329+
330+ ## Authentication
331+
332+ Configure authentication so the "Try It Out" feature can execute protected
333+ procedures. An "Authorize" button appears in the top bar — click it to enter
334+ credentials. Values are stored in localStorage and persist across page
335+ refreshes.
336+
337+ ``` typescript
338+ renderTrpcStudio (appRouter , {
339+ url: " /api/trpc" ,
340+ auth: { type: " bearer" , description: " JWT token from /api/auth/login" },
341+ });
342+ ```
343+
344+ Multiple auth methods:
345+
346+ ``` typescript
347+ renderTrpcStudio (appRouter , {
348+ url: " /api/trpc" ,
349+ auth: [
350+ { type: " bearer" , description: " JWT token" },
351+ {
352+ type: " cookie" ,
353+ name: " next-auth.session-token" ,
354+ description: " NextAuth session" ,
355+ },
356+ { type: " header" , name: " x-api-key" , description: " API key" },
357+ ],
358+ });
359+ ```
360+
361+ ** Supported auth types:**
362+
363+ | Type | Header sent | Use case |
364+ | -------- | ------------------------------- | ----------------- |
365+ | ` bearer ` | ` Authorization: Bearer <value> ` | JWT, OAuth tokens |
366+ | ` basic ` | ` Authorization: Basic <value> ` | Basic auth |
367+ | ` header ` | ` <name>: <value> ` | API keys |
368+ | ` cookie ` | ` Cookie: <name>=<value> ` | Session cookies |
369+
250370## API
251371
252372### ` renderTrpcStudio(router, options) `
@@ -256,17 +376,31 @@ Returns a self-contained HTML string.
256376``` typescript
257377interface RenderOptions {
258378 url: string ; // Base tRPC URL
259- outputSchemas?: Record<string, JsonSchema>; // From CLI extractor
379+ outputSchemas? : Record <string , JsonSchema >; // From CLI: schemas.outputs
380+ inputSchemas? : Record <string , JsonSchema >; // From CLI: schemas.inputs (fallback for z.custom())
260381 transformer? : " superjson" | " none" ; // Default: "none"
382+ auth? : AuthConfig | AuthConfig []; // Authentication config for "Authorize" button
261383 meta? : {
262384 title? : string ;
263385 description? : string ;
264386 version? : string ;
265387 };
266- headers?: Record<string, string>; // Auth headers for "Try It Out"
388+ headers? : Record <string , string >; // Default headers for all requests
389+ }
390+
391+ interface AuthConfig {
392+ type: " bearer" | " cookie" | " header" | " basic" ;
393+ name? : string ; // Required for "cookie" and "header" types
394+ description? : string ; // Shown in the Authorize modal
267395}
268396```
269397
398+ ** How input schema merging works:** Zod runtime schemas are the primary source.
399+ When a field produces an empty schema (e.g., ` z.custom<T>() ` ), the CLI-extracted
400+ schema is used as a fallback. This gives you the best of both worlds — real Zod
401+ validation metadata where available, with TypeScript type info for ` z.custom() `
402+ fields.
403+
270404## Supported Zod Types
271405
272406| Zod Type | Input Form Control | Notes |
0 commit comments