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

@guebbit/openapi-runnable-collections

v0.1.0

Published

Generate API client collections (Bruno, Insomnia, Mockoon, Postman) from an OpenAPI document, filled with values from your seed dataset so every request actually runs.

Readme

@guebbit/openapi-runnable-collections

Generate API client collections — Bruno, Insomnia, Mockoon and Postman — from one OpenAPI document, filled with values from your seed dataset so every request actually runs.

openapi.yaml  +  your seed data  +  your probes   →   contract.bruno.yml
                                                      contract.insomnia.yaml
                                                      contract.mockoon.json
                                                      contract.postman.json

Why this exists

There are good OpenAPI-to-collection converters already. Every one of them turns a schema into a request you cannot send:

{ "productId": "", "quantity": 0 }

So the collection gets imported once, found useless, and abandoned — and the next person hand-edits their own copy, which then rots against the contract it was copied from.

This package fills the same requests from the dataset your API is actually seeded with:

{ "productId": "65dc8a99604c307b702b5ccc", "quantity": 2 }

GET /products/{id} asks for a product that exists. POST /account/login sends credentials that work — and returns an admin token, so the admin-only requests further down the collection do not all 403. That is the difference between a collection you click and one you fix first.

It also carries the half of a collection a contract cannot describe. A spec declares valid calls and their declared answers; the requests that prove your API rejects things are declared as probes and emitted alongside the generated ones.

Three properties worth knowing

Deterministic. Ids are hashed from method and path; timestamps are a fixed instant. Regenerate and you get a byte-identical document unless the contract moved — so you can commit your collections and assert in CI that they match a fresh run. Every other converter mints random UUIDs, which means a diff on every run and a merge conflict on every branch.

Grouping is explicit. Sections are an input, not an inference. Tags look like the right answer until you meet a path tagged with a group that does not own it, or a group spanning two tags — sectionsFromTags is there for the cases where they do line up, and ignored otherwise.

Two output shapes, one content. bundles gives you the whole importable document. sections and scaffolding give you the same content sliced per section, for repos that commit one fragment per module. A bundle is exactly the concatenation of its slices — asserted in the test suite — so which layout you adopt does not change a byte of output.

Install

npm install --save-dev @guebbit/openapi-runnable-collections

Use

import { generateCollections, loadSpec } from '@guebbit/openapi-runnable-collections';
import { writeFileSync } from 'node:fs';

// Records your API is actually seeded with — imported from your seeder, not retyped here.
const seedProduct = { id: '65dc8a99604c307b702b5ccc', title: 'Espresso Machine', price: 249.99 };
const seedUser = { id: '65dc8a99604c307b702b5aaa', email: '[email protected]' };
const seedOrder = { id: '65dc8a99604c307b702b5bbb' };
const deletedOrder = { id: '65dc8a99604c307b702b5ddd' };
const [ADMIN_EMAIL, ADMIN_PASSWORD] = ['[email protected]', 'correct-horse-battery'];
const [USER_EMAIL, USER_PASSWORD] = [seedUser.email, 'correct-horse-battery'];

const { bundles } = generateCollections({
    spec: loadSpec('openapi.yaml'),

    // Which paths belong to which folder, in the order you want them listed.
    sections: [
        { name: 'products', paths: ['/products', '/products/{id}'] },
        { name: 'cart', paths: ['/cart', '/cart/checkout'] },
        { name: 'account', label: 'Account & Auth', paths: ['/account/login'] }
    ],

    // Where a runnable value comes from. Every field is optional.
    values: {
        byProperty: { productId: seedProduct.id, quantity: 2 },
        byEntity: { Product: seedProduct, User: seedUser },
        byOperation: {
            'POST /account/login': { email: ADMIN_EMAIL, password: ADMIN_PASSWORD }
        },
        byFormat: { email: USER_EMAIL, password: USER_PASSWORD },
        pathParam: (name, template) =>
            name === 'id' && template.startsWith('/orders') ? seedOrder.id : undefined,
        tokens: { seedProductId: seedProduct.id, seedDeletedOrderId: deletedOrder.id }
    },

    // Requests the contract cannot describe, per section.
    probes: {
        cart: [
            {
                name: 'Reject a cart quantity of zero',
                why: 'The contract cannot express a body it forbids.',
                method: 'POST',
                path: '/cart',
                auth: 'bearer',
                body: { productId: '{{seedProductId}}', quantity: 0 }
            }
        ]
    },

    collection: { name: 'Ecommerce API', mockoon: { port: 3001 } }
});

writeFileSync('contract.bruno.yml', bundles.bruno!);
writeFileSync('contract.insomnia.yaml', bundles.insomnia!);
writeFileSync('contract.mockoon.json', bundles.mockoon!);
writeFileSync('contract.postman.json', bundles.postman!);

The one line worth checking in the output is POST /cart's body: it carries a product id that EXISTS, not {"productId": "", "quantity": 0}. Import any of the four files and run the login request first to fill in the token — everything below it stops returning 401.

Committing per-section fragments instead

const { sections, trees, scaffolding } = generateCollections({ ...options });

writeFileSync('modules/cart/bruno.yml', sections.bruno!.cart!);
writeFileSync('modules/cart/mockoon.routes.json', sections.mockoon!.cart!);
writeFileSync('modules/cart/mockoon.tree.json', trees.cart!);

Reassemble them yourself with scaffolding.<tool>.header and .footer — YAML slices concatenate verbatim; JSON slices join with ,\n between and none after. Mockoon also gives you scaffolding.mockoon.treeHeader, which sits between the routes and the footer. Or just use bundles, which is that concatenation already — the test suite asserts the two are identical.

How values resolve

Most specific first. The first rule that matches wins:

| # | Source | Matches on | | --- | ---------------------- | ----------------------------------- | | 1 | the schema's example | the contract stated it | | 2 | the schema's default | the contract stated it | | 3 | values.byEntity | a $ref naming a component schema | | 4 | values.byProperty | a leaf property's name | | 5 | values.byFormat | the schema's format | | 6 | the type itself | last resort — a visible placeholder |

values.byOperation sits outside the ladder: it replaces a whole request body for one operation, for the cases a schema cannot get right at all (the login that has to return an admin token).

values.pathParam returning undefined falls through to byProperty, then to the literal example, so you only write the parameters that need deciding.

Probes

# cart/probes.yml
- name: 'Probe: a quantity the schema forbids'
  why: >-
      Zero, where the contract requires at least one. Removing a line has its own endpoint, so a
      zero quantity is a validation failure rather than a shortcut.
  method: POST
  path: /cart
  auth: bearer
  body: { productId: '{{seedProductId}}', quantity: 0 }

Load them with loadProbes('cart/probes.yml').

One file for every section

probes is a plain object, so a repo that files probes per module builds it from one loadProbes call each. For repos that keep them all in one place, loadProbeSections reads that file into the same object:

# probes.yml
cart:
    - name: 'Probe: a quantity the schema forbids'
      why: 'Zero, where the contract requires at least one.'
      method: POST
      path: /cart
      auth: bearer
      body: { productId: '{{seedProductId}}', quantity: 0 }

account:
    - name: 'Probe: login with a password that is not the password'
      why: 'The 401 the contract declares but no valid call can reach.'
      method: POST
      path: /account/login
      body: { email: '{{seedUserEmail}}', password: 'not-it' }
generateCollections({
    ...options,
    probes: loadProbeSections('probes.yml')
});

Both loaders check what they read: a probe missing path, or carrying an auth that is not bearer, throws naming the file and the position rather than surfacing later as a broken request. Neither layout is preferred, and the probes are identical either way.

Three things are deliberate:

  • {{tokens}}, never pasted ids. A probe that hard-codes a seeded id is a copy of the dataset, and copies drift. An unknown token throws — a probe silently sending the literal {{typo}} would pass every check and test nothing.
  • No declared responses. A probe exists to show what the API does with an input nobody described. Inventing the answer here would defeat sending it.
  • Not emitted to Mockoon. A mock server answers requests; it does not send them.

Response envelopes

If your API wraps every response — { success, status, message, data } — a schema-shaped example puts the type's placeholder in status and message, giving you a 404 body claiming "status": 1. The default envelope restates the real HTTP code and the response's own description. A body that declares neither field is passed through untouched, so an API without an envelope pays nothing. Override with envelope: (body, status, label) => ….

API

| Export | What it does | | --------------------- | ----------------------------------------------------------------- | | generateCollections | the entry point; returns bundles, sections, trees and scaffolding | | loadSpec | read and parse an OpenAPI document (YAML or JSON) | | loadProbes | read one section's probe file | | loadProbeSections | read every section's probes out of one file | | sectionsFromTags | build sections from tags, when tags are your grouping | | dereference | follow a local #/components/… pointer | | exampleFor | the value resolver, if you want it directly | | defaultEnvelope | the default envelope fixer, to wrap rather than replace | | stableId | the derived-id helper the emitters use | | stableUuid | the same, in the v4-shaped form Mockoon and Postman validate |

Full types ship with the package — GenerateOptions, Section, ValueSources, Probe, CollectionRequest and the rest are all exported.

Notes and limits

  • Bundle your spec first. $ref resolution is local-only: this reads a single document, where a remote pointer cannot occur. Use @redocly/cli bundle or your own concatenation upstream. A pointer that resolves to nothing, or to itself, throws with the pointer named.
  • Bruno's single-document opencollection format, not a folder of .bru files. One file to commit, diff and assert.
  • Insomnia v5 YAML, which is a different shape from Postman v2.1 — hence four emitters rather than three plus a rename.
  • application/json request and response bodies. Other media types are not synthesised.

Requirements

Node 20 or newer. Ships as both ESM and CommonJS, with declarations for each, so import and require both work.

Development

npm run complete:check runs the whole gate the way CI does, and is the pre-commit hook. The layers it is made of, each catching something the others cannot:

| Command | What it catches | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | npm test | Behaviour, byte-identity against the golden fixtures, and invariants over generated contracts | | npm run test:coverage | A branch no case reaches | | npm run typecheck | Signatures widening to any — this is what enforces tests/types.test.ts, since ts-jest runs transpile-only | | npm run test:pack | A broken exports map or a missing files entry, by installing a real tarball and importing it both ways | | npm run test:mutation | Tests that run code without asserting on it. Gated per file by mutation-baseline.json; concurrency comes from .env, see .env-example |

Two things about the harness are worth knowing before trusting a green run, because both once produced a false all-clear. Work done while a test file is being collected belongs to no test under Stryker's per-test coverage, so anything generated at module or describe scope goes silent — see the note in tests/integration/generate.test.ts. And enableFindRelatedTests matches nothing in this repo, which hid the golden suite from every mutant — see the note in stryker.config.json. A test nobody has watched fail is not evidence: break the source on purpose and confirm it goes red.

License

MIT