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

smart-pact

v0.2.3

Published

Point at your typed API routes. Get pact files. No wrappers. No manual contracts.

Readme

smart-pact

Point at your typed routes. Get pact files. Zero manual matchers.

smart-pact reads your TypeScript types directly via the compiler API and generates Pact v2 contract files — including request matchers, response matchers, error scenarios, and broker integration — without any hand-written matcher code.


The problem with standard Pact

Standard Pact requires you to write matchers by hand:

// Standard Pact — you write this for every field
.withRequest({
  body: {
    id: integer(),
    name: like("Alice"),
    status: term({ generate: "ACTIVE", matcher: "^(ACTIVE|INACTIVE)$" })
  }
})

Your types already say id: number, name: string, status: "ACTIVE" | "INACTIVE". You're writing the same information twice.

smart-pact reads your TypeScript types directly and generates the matchers for you.


Install

npm install smart-pact

smart-pact uses your project's own typescript installation — no version conflicts.


How it works

1. Write a routes file — the only file you write:

// pact.routes.ts
import { defineRoute, defineScenario } from 'smart-pact';
import type { User, CreateUserBody, ApiError } from './your-existing-types';

export default [
  defineRoute<{ id: number }, User>({
    description: "fetch user by ID",
    method: "GET",
    path: "/users/:id",
    service: "user-service",
    providerState: "user with ID 1 exists",
    inputExample:  { id: 42 },
    outputExample: { id: 42, name: "Alice", status: "ACTIVE" },
  }),

  defineScenario<{ id: number }, ApiError>({
    description: "fetch user — not found",
    method: "GET",
    path: "/users/:id",
    service: "user-service",
    status: 404,
    providerState: "user with ID 999 does not exist",
    inputExample: { id: 999 },
    errorExample: { code: "USER_NOT_FOUND", message: "User 999 was not found" },
  }),

  defineRoute<CreateUserBody, User>({
    description: "create a user",
    method: "POST",
    path: "/users",
    service: "user-service",
    successStatus: 201,
    inputExample:  { name: "Bob Smith", email: "[email protected]" },
    outputExample: { id: 1, name: "Bob Smith", status: "ACTIVE" },
  }),
];

2. Run the CLI:

npx smart-pact generate \
  --routes ./pact.routes.ts \
  --consumer frontend \
  --out ./pacts

3. Get pact files with realistic example values:

{
  "request":  { "method": "GET", "path": "/users/42" },
  "response": {
    "body": {
      "id":        { "match": "integer",   "value": 42 },
      "name":      { "match": "type",      "value": "Alice" },
      "status":    { "match": "regex",     "regex": "^(ACTIVE|INACTIVE)$", "value": "ACTIVE" },
      "createdAt": { "match": "timestamp", "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "value": "2026-01-01T00:00:00.000Z" }
    }
  }
}

No hand-written matchers. No duplicate effort. Types drive the contract, examples make it readable.


Type → Matcher mapping

| TypeScript type | Pact matcher | |------------------------------------|---------------------------------------| | number (integer) | integer | | number (decimal) | decimal | | string | type (like) | | boolean | boolean | | null | null | | Date | timestamp (ISO 8601) | | "ACTIVE" \| "INACTIVE" | regex ^(ACTIVE\|INACTIVE)$ | | T \| null | T's matcher (null stripped) | | T[] / Array<T> | eachLike (min: 1) | | Nested object / interface | Recursive body matchers | | field?: T (optional) | Included, skipped if unresolvable | | Record<string, V> | One example key with V's matcher | | any | ❌ compile error | | unknown | ❌ scan error | | undefined (explicit) | ❌ scan error (use field?: T) |

any is banned at both compile time (via a generic guard in defineRoute) and at scan time. This ensures every field in your contract has a deterministic matcher.


defineRoute config

defineRoute<TInput, TOutput>({
  description:      string,                    // required — human label for this interaction
  method:           HttpMethod,                // required — GET | POST | PUT | PATCH | DELETE
  path:             string,                    // required — "/users/:id" (:params from TInput)
  service:          string,                    // required — must match provider name in verify

  providerState?:   string,                    // Pact "Given" clause
  successStatus?:   HttpStatusCode,            // default 200
  requestHeaders?:  Record<string, string>,
  responseHeaders?: Record<string, string>,
  liveUrl?:         string,                    // optional live URL for response examples

  inputExample?:    Record<string, unknown>,   // example values for request path params + body
  outputExample?:   Record<string, unknown>,   // example values for response body matchers
})

Path parameters and inputExample

Path parameters (:param placeholders in path) are interpolated from inputExample. When a matching key exists, it replaces the placeholder in the pact's example request path; without it, numeric params fall back to "1".

defineRoute<{ id: number }, User>({
  path: "/users/:id",
  inputExample: { id: 42 },   // → request path: /users/42
  // no inputExample          // → request path: /users/1
})

For POST/PUT/PATCH routes, non-path-param fields from inputExample are used as the value inside request body matchers. For GET/DELETE routes they populate example query string values.

Response body and outputExample

outputExample sets the value field inside each generated response matcher. The matcher type always comes from TOutput via the TypeScript compiler — outputExample only controls what concrete value appears in the pact file.

defineRoute<{ id: number }, User>({
  outputExample: {
    id: 42,           // integer matcher, value: 42      (not the default 1)
    name: "Alice",    // type matcher,    value: "Alice" (not "example")
    status: "ACTIVE", // regex matcher,   value: "ACTIVE" (picked from the enum)
  },
})

Without outputExample, values fall back to generic defaults: 1 for numbers, "example" for strings, true for booleans, and the first member for string literal unions.


defineScenario — error and edge-case paths

defineScenario defines an error or edge-case response for an existing route. It is matched to a parent route by path + method + service and emitted as an additional Pact interaction in the same file.

defineScenario<TInput, TError>({
  description:    string,                    // required
  method:         HttpMethod,                // required
  path:           string,                    // required — must match a defineRoute
  service:        string,                    // required
  status:         HttpStatusCode,            // required — typically 4xx/5xx

  providerState?: string,
  requestHeaders?: Record<string, string>,

  inputExample?:  Record<string, unknown>,   // example values for request path params + body
  errorExample?:  Record<string, unknown>,   // example values for error response body matchers
})

TError is scanned exactly like TOutput in defineRoute — full recursive matcher generation from your error response types.

inputExample and errorExample on a scenario are independent from the parent route's examples. Each scenario controls its own path interpolation and response values separately:

defineRoute<{ id: number }, User>({
  path: "/users/:id",
  inputExample:  { id: 10 },
  outputExample: { id: 10, name: "Alice" },
}),

defineScenario<{ id: number }, ApiError>({
  path: "/users/:id",
  status: 404,
  inputExample: { id: 999 },    // scenario path: /users/999 (independent of parent)
  errorExample: { code: "USER_NOT_FOUND", message: "User 999 was not found" },
}),

Provider verification

Run this against a live provider to confirm it satisfies every interaction in the pact files:

npx smart-pact verify \
  --provider user-service \
  --provider-url http://localhost:3001 \
  --pact-dir ./pacts

Output:

[smart-pact] Verifying 3 interactions — frontend → user-service
  • "fetch user by ID" ... ✅
  • "fetch user — not found" ... ✅
  • "create a user" ... ✅

─────────────────────────────────────────
[smart-pact] Verification Summary
─────────────────────────────────────────
✅  frontend → user-service: 3/3 passed
─────────────────────────────────────────
Total: 3 passed, 0 failed

Provider states

When a providerState is set on a route or scenario, the verifier POSTs to the --state-url endpoint before each interaction so the provider can seed the right fixture:

// Express example
app.post("/_pact/state", async (req, res) => {
  const { state } = req.body;
  await seedDatabase(state);
  res.json({ ok: true });
});
npx smart-pact verify \
  --provider user-service \
  --provider-url http://localhost:3001 \
  --state-url http://localhost:3001/_pact/state \
  --pact-dir ./pacts

Pact Broker integration

Publish pacts

npx smart-pact publish \
  --pact-dir ./pacts \
  --consumer-version 1.2.3 \
  --broker-url https://broker.example.com \
  --broker-token $PACT_BROKER_TOKEN

Basic auth is also supported via --broker-username and --broker-password.

Can-I-Deploy

npx smart-pact can-i-deploy \
  --pacticipant frontend \
  --version 1.2.3 \
  --broker-url https://broker.example.com \
  --broker-token $PACT_BROKER_TOKEN

Programmatic broker usage

import { PactBrokerClient, printPublishSummary, printCanIDeployResult } from 'smart-pact';

const client = new PactBrokerClient({
  brokerBaseUrl: "https://broker.example.com",
  brokerToken: process.env.PACT_BROKER_TOKEN,
  // Or: brokerUsername / brokerPassword for basic auth
});

const results = await client.publishPacts("./pacts", "1.2.3");
printPublishSummary(results);

const deploy = await client.canIDeploy("frontend", "1.2.3");
printCanIDeployResult(deploy);

// Fetch latest pacts for a provider from the broker (optionally write to disk)
const pacts = await client.fetchPacts("user-service", "./pacts");

Inspect pact files

npx smart-pact summary --pact-dir ./pacts

CLI reference

smart-pact — Type-driven consumer contract testing

Commands:
  generate      Scan a routes file and write pact files
  verify        Verify pact files against a live provider
  publish       Publish pact files to a Pact Broker
  can-i-deploy  Check whether a version is safe to deploy
  summary       Inspect pact files

Generate options:
  --routes        Path to pact.routes.ts             (required)
  --consumer      Consumer service name               (required)
  --out           Output directory, default: ./pacts
  --tsconfig      Path to tsconfig.json (auto-detected if omitted)

Verify options:
  --pact-dir      Directory of pact files, default: ./pacts
  --provider      Provider service name               (required)
  --provider-url  Base URL of running provider        (required)
  --state-url     Provider state setup endpoint       (optional)
  --broker-url    Pull pacts from broker instead of disk (optional)

Publish options:
  --pact-dir          Directory of pact files, default: ./pacts
  --consumer-version  Semver version of the consumer (required)
  --broker-url        Base URL of Pact Broker         (required)
  --broker-token      Bearer token for broker auth    (optional)
  --broker-username / --broker-password               (optional)

Can-I-Deploy options:
  --pacticipant   Consumer or provider name           (required)
  --version       Version to check                   (required)
  --broker-url    Base URL of Pact Broker             (required)
  --broker-token  Bearer token for broker auth        (optional)
  --broker-username / --broker-password               (optional)

Summary options:
  --pact-dir      Directory of pact files, default: ./pacts

CI integration

# .github/workflows/contracts.yml
- name: Generate pacts
  run: |
    npx smart-pact generate \
      --routes ./src/pact.routes.ts \
      --consumer frontend \
      --out ./pacts

- name: Publish pacts to broker
  run: |
    npx smart-pact publish \
      --consumer-version ${{ github.sha }} \
      --broker-url ${{ secrets.PACT_BROKER_URL }} \
      --broker-token ${{ secrets.PACT_BROKER_TOKEN }}

- name: Can I deploy?
  run: |
    npx smart-pact can-i-deploy \
      --pacticipant frontend \
      --version ${{ github.sha }} \
      --broker-url ${{ secrets.PACT_BROKER_URL }} \
      --broker-token ${{ secrets.PACT_BROKER_TOKEN }}

# Provider side:
- name: Verify contracts
  run: |
    npx smart-pact verify \
      --provider user-service \
      --provider-url ${{ env.API_URL }} \
      --state-url ${{ env.API_URL }}/_pact/state

Programmatic API

import {
  defineRoute, defineScenario,
  generatePacts, scanRoutesFile,
  verifyPacts, printVerificationSummary,
  shapeToPactBody,
  PactBrokerClient, printPublishSummary, printCanIDeployResult,
} from 'smart-pact';

Key exported types

import type {
  RouteDefinition, ScenarioDefinition,
  ResolvedRoute, ResolvedScenario,
  TypeShape, PactFile, PactInteraction, PactBody, PactMatcher,
  SmartPactConfig, VerifyConfig, VerificationResult, InteractionResult,
  PactBrokerConfig, PublishResult, CanIDeployResult, BrokerVerification,
  HttpMethod, HttpStatusCode,
} from 'smart-pact';

How the scanner works

smart-pact uses the TypeScript compiler API to read your types at scan time — not at runtime. This is the only approach that produces correct matchers.

Runtime inference from example values is wrong: a string field that happens to contain a UUID would get a regex UUID matcher, but the type says string, so the correct matcher is type. Types reflect developer intent; example values reflect one specific instance. inputExample, outputExample, and errorExample exist only to populate the value fields inside matchers — they have no effect on which matcher type is chosen.

The scanner resolves TInput and TOutput from your defineRoute calls, walks the compiler's type graph recursively, and builds an intermediate TypeShape representation. TypeShape is then converted to Pact matchers by shapeToPactBody, with the example values threaded in per field. The full type graph — including interfaces from other files, generic expansions, and intersections — is resolved, provided all files are reachable from your tsconfig.json.

tsconfig.json is auto-detected by walking up the directory tree from the routes file. Pass --tsconfig to override.


Known limitations (v0.2.x)

| Limitation | Notes | |---|---| | Pact spec v2 only | Matchers are inline in the body; v3 matchingRules not yet supported | | Response headers written but not verified | Headers appear in pact files but are not evaluated during verify | | Record<string, V> produces one example key | Safe but imprecise for open-ended maps | | Types from node_modules may not resolve | Requires the package to be included in your tsconfig compilation unit | | Circular type references capped at depth 20 | Very deep recursive types are truncated |


License

MIT