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

trpc-diff

v1.2.0

Published

Detect breaking changes in tRPC routers

Readme

What it does

trpc-diff converts your tRPC router to an OpenAPI contract and diffs two contracts to find breaking changes.

Under the hood it uses:

  • zod-openapi to generate the OpenAPI contract from your Zod schemas
  • oasdiff-js (JavaScript bindings for oasdiff) to detect breaking changes between contracts

Install

npm install -D trpc-diff
# or
bun add -D trpc-diff

Make sure you have @trpc/server installed (peer dependency).

Usage

Import your router, generate a contract, and diff it against another version.

Generate a contract

import { generateContract, zodAdapter } from "trpc-diff";
import { appRouter } from "./server/router";

const contract = generateContract(appRouter, [zodAdapter]);

// write to disk or pass directly to diffContracts
await Bun.write("contract.json", JSON.stringify(contract, null, 2));

You must provide at least one schema adapter. zodAdapter is included for Zod schemas. You can also bring your own adapter for other parsers (e.g. Valibot, ArkType, or custom validators).

Custom adapters

If your router uses multiple parser libraries, you can provide multiple adapters:

const contract = generateContract(appRouter, [zodAdapter, myValibotAdapter]);

An adapter implements the IParserAdapter<TParser> interface:

export interface IParserAdapter<TParser = unknown> {
  isParser(value: unknown): value is TParser;
  mergeInputs(inputs: TParser[]): TParser | null;
  toSchema(parser: TParser, io: "input" | "output"): unknown;
}

If a procedure chains inputs from different parser families, their resulting OpenAPI schemas are merged with { allOf: [...] }.

Diff two contracts

import { diffContracts } from "trpc-diff";
import base from "./contract-base.json";
import head from "./contract-head.json";

const result = await diffContracts(base, head);

if (!result.compatible) {
  console.log("Breaking changes found:");
  for (const finding of result.findings) {
    console.log(`- ${finding.code} at ${finding.entity}`);
  }
}

Options

Custom severity levels

const result = await diffContracts(base, head, {
  severityLevels: {
    // treat removing request properties as breaking
    "request-property-removed": "err",
    // ignore response enum value additions
    "response-body-enum-value-added": "none",
  },
});

Each key is a check id and each value is a level (err, warn, info, or none). This emulates an oasdiff-levels.txt file.

Defaults

By default, only request-property-removed is overridden to none (removing request properties is considered compatible). This matches typical tRPC behavior where Zod input schemas are non-strict by default, so removing a field from the schema doesn't necessarily break existing clients.

Everything else follows oasdiff's default severity levels.

To see all available check ids and their default levels, install @oasdiff-js/oasdiff-js directly and run:

npx oasdiff checks

Exit on missing adapter

By default, if a procedure uses a parser that none of the provided adapters can handle, generateContract will throw and exit. You can make it skip and log it instead:

const contract = generateContract(appRouter, [zodAdapter], {
  exitOnMissingAdapter: false,
});

ESLint rule

trpc-diff can only generate accurate response contracts when procedures explicitly declare .output(). tRPC can infer outputs from resolver return types, but that type information is not available from the runtime router metadata used by this library.

You can opt into the bundled ESLint rule to enforce explicit outputs:

import trpcDiff from "trpc-diff/eslint";

export default [
  {
    plugins: {
      "trpc-diff": trpcDiff,
    },
    rules: {
      "trpc-diff/require-output": "error",
    },
  },
];

If a procedure intentionally has no output, use .output(z.void()).

Using it in CI

Since the library is runtime-agnostic, you can diff PRs in CI by generating contracts in each checkout with the same runtime your app uses.

- name: Generate base contract
  run: bun run scripts/generate-contract.ts --out base.json

- name: Generate head contract
  run: bun run scripts/generate-contract.ts --out head.json

- name: Diff contracts
  run: bun run scripts/diff-contracts.ts --base base.json --head head.json

API reference

generateContract(router, adapters, options?): IOpenApiDocument

Converts a tRPC router to an OpenAPI 3.0 contract.

| Parameter | Type | Description | | ------------------------------ | ------------------ | ---------------------------------------------------- | | router | AnyRouter | tRPC router | | adapters | IParserAdapter[] | Adapters for parsing input/output schemas | | options.exitOnMissingAdapter | boolean | Throw when a parser has no adapter (default: true) |

diffContracts(base, head, options?): Promise<IDiffResult>

Diffs two OpenAPI contracts for breaking changes.

| Parameter | Type | Description | | ------------------------ | ------------------------ | --------------------------- | | base | IOpenApiDocument | Base contract | | head | IOpenApiDocument | Head contract | | options.severityLevels | Record<string, string> | Optional severity overrides |

Returns { compatible: boolean; findings: IDiffFinding[] }.

Design

trpc-diff is a library and not a CLI tool because tRPC routers are runtime values.

Extracting their schemas requires executing the module that defines them, which in turn requires the correct runtime, module resolver, and loader for your specific project. Rather than bundling a TypeScript loader or importing user code on your behalf, the library exposes the primitives so you can run them in your own runtime context.

License

MIT