npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@srawad/trpc-studio

v0.2.0

Published

Swagger-like UI for tRPC — auto-generated input forms, Try It Out execution, output type visualization, and a CLI for static type extraction

Readme

trpc-studio

A Swagger-like developer UI for tRPC APIs — auto-generated input forms, "Try It Out" execution, output type visualization, and a CLI for static type extraction via the TypeScript Compiler API.

Features

  • Auto-generated input forms from Zod schemas — string, number, boolean, enum, object, array
  • "Try It Out" — execute real tRPC calls from the browser with response display, timing, and status
  • Output type visualization — automatic runtime detection from .output() schemas, or static extraction via the CLI using the TypeScript Compiler API
  • Sidebar navigation — procedures grouped by router, color-coded badges (query/mutation/subscription), real-time search, tag filtering
  • Authentication — configurable "Authorize" button (bearer, cookie, header, basic) with localStorage persistence
  • Procedure metadata — automatically displays .meta() values (auth, deprecated, tags, custom fields)
  • Self-contained — served as a single HTML response, no static file hosting needed
  • tRPC v10 & v11 compatible

Installation

npm install -D @srawad/trpc-studio

Quick Start

Express

import express from "express";
import * as trpcExpress from "@trpc/server/adapters/express";
import { renderTrpcStudio } from "@srawad/trpc-studio";
import { appRouter } from "./router";

const app = express();

app.use("/trpc", trpcExpress.createExpressMiddleware({ router: appRouter }));

app.get("/studio", (_req, res) => {
  res.send(
    renderTrpcStudio(appRouter, {
      url: "http://localhost:3000/trpc",
      meta: { title: "My API", version: "1.0.0" },
    }),
  );
});

app.listen(3000);

Open http://localhost:3000/studio in your browser.

Next.js (App Router)

// src/app/api/studio/route.ts
import { NextResponse } from "next/server";
import { appRouter } from "~/server/router";

export async function GET() {
  if (process.env.NODE_ENV !== "development") {
    return new NextResponse("Not Found", { status: 404 });
  }

  const { renderTrpcStudio } = await import("@srawad/trpc-studio");

  return new NextResponse(
    renderTrpcStudio(appRouter, {
      url: "/api/trpc",
      transformer: "superjson",
      meta: { title: "My API" },
    }),
    { status: 200, headers: { "Content-Type": "text/html" } },
  );
}

Output Type Visualization

trpc-studio shows output (response) schemas in two ways — runtime and static extraction. You can use either or both.

Option A: Runtime (automatic, zero config)

If your procedures use .output() with a Zod schema, trpc-studio picks it up automatically at runtime — no extra setup needed:

// This procedure's output schema is detected automatically
const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .output(z.object({ id: z.string(), name: z.string(), email: z.string() }))
    .query(({ input }) => {
      return db.user.findUnique({ where: { id: input.id } });
    }),
});

Option B: CLI Extractor (for procedures without .output())

Most tRPC procedures don't use .output() — they just return a value and let TypeScript infer the type. For these, trpc-studio provides a CLI tool that uses the TypeScript Compiler API to statically analyze your router source code and extract the return types.

Step 1: Run the extractor

npx @srawad/trpc-studio extract --router ./src/server/router.ts

This analyzes your TypeScript source and generates a .trpc-studio.json file with JSON Schema definitions for every procedure's input and output types.

This is especially useful for:

  • Output types — procedures that don't use .output() (most tRPC code)
  • Input types with z.custom<T>() — Zod's z.custom() carries no runtime schema, so the UI renders it as an empty object. The CLI extractor resolves the actual TypeScript type and provides full structural info.

Step 2: Pass the schemas to renderTrpcStudio

import schemas from "./.trpc-studio.json";

app.get("/studio", (_req, res) => {
  res.send(
    renderTrpcStudio(appRouter, {
      url: "http://localhost:3000/trpc",
      inputSchemas: schemas.inputs,
      outputSchemas: schemas.outputs,
    }),
  );
});

Full Example

Given this router:

// src/server/router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();

export const appRouter = t.router({
  hello: t.procedure
    .input(z.object({ name: z.string().optional() }))
    .query(({ input }) => {
      return { greeting: `Hello ${input.name ?? "world"}!` };
    }),

  user: t.router({
    getById: t.procedure
      .input(z.object({ id: z.string() }))
      .query(({ input }) => {
        return { id: input.id, name: "John Doe", email: "[email protected]" };
      }),

    create: t.procedure
      .input(z.object({ name: z.string(), email: z.string() }))
      .mutation(({ input }) => {
        return { id: "new-id", ...input, createdAt: new Date().toISOString() };
      }),
  }),
});

Run the extractor:

npx @srawad/trpc-studio extract \
  --router ./src/server/router.ts \
  --tsconfig ./tsconfig.json \
  --name appRouter \
  --out .trpc-studio.json

This generates .trpc-studio.json with both input and output schemas:

{
  "inputs": {
    "hello": {
      "type": "object",
      "properties": {
        "name": { "type": "string" }
      }
    },
    "user.getById": {
      "type": "object",
      "properties": {
        "id": { "type": "string" }
      },
      "required": ["id"]
    },
    "user.create": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "email": { "type": "string" }
      },
      "required": ["name", "email"]
    }
  },
  "outputs": {
    "hello": {
      "type": "object",
      "properties": {
        "greeting": { "type": "string" }
      },
      "required": ["greeting"]
    },
    "user.getById": {
      "type": "object",
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "email": { "type": "string" }
      },
      "required": ["id", "name", "email"]
    },
    "user.create": {
      "type": "object",
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "email": { "type": "string" },
        "createdAt": { "type": "string" }
      },
      "required": ["id", "name", "email", "createdAt"]
    }
  }
}

Then wire it up:

import schemas from "./.trpc-studio.json";

app.get("/studio", (_req, res) => {
  res.send(
    renderTrpcStudio(appRouter, {
      url: "http://localhost:3000/trpc",
      inputSchemas: schemas.inputs,
      outputSchemas: schemas.outputs,
      meta: { title: "My API", version: "1.0.0" },
    }),
  );
});

Tip: Add .trpc-studio.json to your .gitignore and run the extractor as part of your dev/build script:

{
  "scripts": {
    "dev": "npx @srawad/trpc-studio extract --router ./src/server/router.ts && next dev",
    "studio:extract": "npx @srawad/trpc-studio extract --router ./src/server/router.ts"
  }
}

CLI Options

| Option | Default | Description | | ------------------- | ------------------- | ------------------------------------- | | --router <path> | (required) | Path to the file exporting the router | | --out <path> | .trpc-studio.json | Output file path | | --tsconfig <path> | ./tsconfig.json | Path to tsconfig.json | | --name <name> | appRouter | Name of the exported router variable | | --help | | Show help message |

Procedure Metadata (.meta())

trpc-studio automatically reads and displays tRPC's built-in .meta() on each procedure. No configuration needed — if your procedures have meta, it shows up.

const t = initTRPC
  .meta<{
    auth?: boolean;
    deprecated?: boolean;
    rateLimit?: number;
    tags?: string[];
  }>()
  .create();

const protectedProcedure = t.procedure.meta({ auth: true }).use(authMiddleware);

export const appRouter = t.router({
  getUser: protectedProcedure
    .meta({ tags: ["billing", "v2"] })
    .input(z.object({ id: z.string() }))
    .query(/* ... */),

  legacyReport: t.procedure
    .meta({ deprecated: true, auth: true, tags: ["reporting"] })
    .query(/* ... */),
});

In the UI:

  • Sidebar — 🔒 icon for auth: true, ⚠️ icon for deprecated: true, tag badges next to procedure names
  • Detail panel — all meta keys rendered as badge pills below the procedure header (e.g., 🔒 auth, ⚠️ deprecated, rateLimit: 100)
  • Tag filtering — if any procedures use meta.tags, a filter bar appears at the top of the sidebar. Click tags to filter procedures across all routers. Multi-select supported.

The rendering is generic — trpc-studio doesn't interpret the meta values, it just displays whatever keys and values are present.

Authentication

Configure authentication so the "Try It Out" feature can execute protected procedures. An "Authorize" button appears in the top bar — click it to enter credentials. Values are stored in localStorage and persist across page refreshes.

renderTrpcStudio(appRouter, {
  url: "/api/trpc",
  auth: { type: "bearer", description: "JWT token from /api/auth/login" },
});

Multiple auth methods:

renderTrpcStudio(appRouter, {
  url: "/api/trpc",
  auth: [
    { type: "bearer", description: "JWT token" },
    {
      type: "cookie",
      name: "next-auth.session-token",
      description: "NextAuth session",
    },
    { type: "header", name: "x-api-key", description: "API key" },
  ],
});

Supported auth types:

| Type | Header sent | Use case | | -------- | ------------------------------- | ----------------- | | bearer | Authorization: Bearer <value> | JWT, OAuth tokens | | basic | Authorization: Basic <value> | Basic auth | | header | <name>: <value> | API keys | | cookie | Cookie: <name>=<value> | Session cookies |

API

renderTrpcStudio(router, options)

Returns a self-contained HTML string.

interface RenderOptions {
  url: string; // Base tRPC URL
  outputSchemas?: Record<string, JsonSchema>; // From CLI: schemas.outputs
  inputSchemas?: Record<string, JsonSchema>; // From CLI: schemas.inputs (fallback for z.custom())
  transformer?: "superjson" | "none"; // Default: "none"
  auth?: AuthConfig | AuthConfig[]; // Authentication config for "Authorize" button
  meta?: {
    title?: string;
    description?: string;
    version?: string;
  };
  headers?: Record<string, string>; // Default headers for all requests
}

interface AuthConfig {
  type: "bearer" | "cookie" | "header" | "basic";
  name?: string; // Required for "cookie" and "header" types
  description?: string; // Shown in the Authorize modal
}

How input schema merging works: Zod runtime schemas are the primary source. When a field produces an empty schema (e.g., z.custom<T>()), the CLI-extracted schema is used as a fallback. This gives you the best of both worlds — real Zod validation metadata where available, with TypeScript type info for z.custom() fields.

Supported Zod Types

| Zod Type | Input Form Control | Notes | | ------------------------ | ------------------- | ------------------ | | z.string() | Text input | | | z.number() | Number input | | | z.boolean() | Checkbox | | | z.enum() | Select dropdown | | | z.object() | Nested fieldset | Recursive | | z.array() | Repeatable group | Add/remove buttons | | z.optional() | Marked as optional | | | z.default() | Shows default value | In placeholder | | z.string().email() | Text input | | | z.number().min().max() | Number input | |

Comparison

| Feature | trpc-studio | trpc-panel | trpc-openapi | | ------------------------- | :---------: | :--------: | :-------------: | | Input forms from Zod | Yes | Yes | No | | Output type visualization | Yes | No | Partial | | "Try It Out" execution | Yes | Yes | Via Swagger | | Self-contained HTML | Yes | No | No | | No code changes required | Yes | Yes | No (decorators) | | tRPC v10 support | Yes | Yes | Yes | | tRPC v11 support | Yes | Partial | Partial | | TypeScript Compiler API | Yes | No | No |

Development

# Clone and install
git clone https://github.com/sayefdeen/trpc-studio
cd trpc-studio
pnpm install

# Build all packages
pnpm build

# Run the example
pnpm --filter express-example dev

# Lint, format, typecheck
pnpm lint
pnpm format:check
pnpm typecheck

Project Structure

packages/
  core/     — TypeScript Compiler API type extractor
  ui/       — React + Tailwind frontend
  server/   — Runtime introspection, HTML rendering, CLI
examples/
  express/  — Express example
  nextjs/   — Next.js example

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Make your changes
  4. Run pnpm lint && pnpm typecheck && pnpm build
  5. Open a pull request

License

MIT