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

bluecat

v2.0.0

Published

Library for building RESTful API HTTP requests, best for generic RESTful API Test Framework

Readme

Bluecat

NPM version Bluecat CI Downloads

Bluecat is a configuration-driven HTTP client for building RESTful API test frameworks. You describe your API surface as a JSON tree once, and Bluecat generates an ergonomic, chainable client that maintains cookies, sessions, and headers under the hood.

  • Define your API in config/api.json — Bluecat builds the client from it
  • async / await based — no fibers, no callback nesting
  • Built on undici and tough-cookie
  • Per-instance cookie jars (no module-level state pollution)
  • Request / response hooks for tracing, auth refresh, retries, etc.
  • Optional OpenAPI 3 adapter — convert any spec to a Bluecat tree
  • Built-in api.json schema validation with friendly errors
  • TypeScript declarations included

v2.0 — breaking changes from v1.x The legacy fibers-based ServiceSync.run(fn) style is gone. All request methods now return Promise<RequestResult>. The names ServiceSync and ServiceAsync are kept as aliases of the new unified Service class so existing imports keep working — but call sites need await. If you must stay on the old fiber-based API, pin to [email protected].

Table of contents


Installation

npm install bluecat

Requires Node.js ≥ 18.


Quick example

POST /post then GET /get against httpbin.org:

config/api.json

{
  "api": {
    "post": { "schema": "https", "method": ["POST"] },
    "get":  { "schema": "https", "method": ["GET"]  }
  }
}
const { expect } = require('chai');
const Bluecat = require('bluecat');

const service = new Bluecat.Service(Bluecat.Api('api'), 'httpbin.org');

it('round-trips a JSON body', async () => {
  const r1 = await service.post.POST({
    body: { cartId: 'test-cart-id' }
  });
  expect(r1.data.statusCode).to.equal(200);
  expect(r1.data.body.json.cartId).to.equal('test-cart-id');

  // any cookies set by the response are auto-sent on the next call
  const r2 = await service.get.GET({ query: { hello: 'world' } });
  expect(r2.data.statusCode).to.equal(200);
});

Examples

Query / headers

await service.search.fitness.result.PUT({
  body:    { term: 'testTerm' },
  query:   { start: 0, limit: 50, error: true },
  headers: { 'User-Agent': 'automation' }
});

URL segments that are not valid JS identifiers

// /cart/v1/add-item/item
await service.cart.v1['add-item'].item.GET();

URL parameters

config/api.json

{
  "api": {
    "checkout": {
      "${uuid}": {
        "contract": { "schema": "https", "method": ["GET"] }
      }
    }
  }
}
await service.checkout['${uuid}'].contract.GET({
  params: { uuid: '5e58...c5c7b' }
});

Full sample test framework

examples/


Request options

Every generated method takes a single options object. All fields are optional.

| Field | Type | Notes | |-----------|-----------------------|--------------------------------------------------------------------------------| | body | object \| string \| Buffer | Request payload. Object bodies are auto-serialized based on Content-Type: application/json (default) → JSON.stringify; application/x-www-form-urlencodedURLSearchParams. Buffer / Uint8Array is sent as-is. | | query | object | Appended as a query string with URLSearchParams. Merges cleanly with an existing ?…. | | params | object | Substitutes ${name} placeholders in the URI; values are URL-encoded. | | headers | object | Per-call headers, merged on top of any tree-level / fixed headers. Values may be strings, sync functions, or async functions (see below). |

Headers can be functions for per-call dynamic values (e.g. signed auth headers). Returning an object expands into multiple headers:

await service.checkout.contract.POST({
  headers: {
    Authorization: () => `Bearer ${token()}`,
    'X-Multi':     async () => ({ 'X-Trace': '1', 'X-Span': '2' })
  }
});

Response shape and error handling

Every request resolves (never rejects):

{
  request: { method, uri, headers, body },
  data:    { statusCode, headers, body, elapsedTime } | undefined,
  err:     Error | null
}

data.body is auto-decoded by Content-Type:

  • application/json → parsed object
  • image/* / application/octet-streamBuffer
  • anything else → string (with a best-effort JSON parse for legacy APIs that forget Content-Type)

Two error categories to distinguish:

const r = await service.users.GET();

if (r.err) {
  // network / DNS / TLS / connection-reset failures — no HTTP exchange happened
  throw r.err;
}

if (r.data.statusCode >= 400) {
  // the server answered, but with a 4xx/5xx — inspect r.data.body
  console.error('API error', r.data.statusCode, r.data.body);
}

Methods do not throw on non-2xx so test assertions read top-down.


Service API

new Bluecat.Service(api, host, options?)

Create a service. options accepts:

| Option | Type | Default | Notes | |--------------|-----------------------|---------|-------------------------------------------------------------| | proxy | string | — | HTTP proxy URI | | strictSSL | boolean | true | Set false to disable TLS verification (testing only) | | dispatcher | undici.Dispatcher | — | Custom dispatcher; takes precedence over proxy/strictSSL. Use this for HTTP/2, custom CAs, mTLS, retry agents, etc. |

const service = new Bluecat.Service(Bluecat.Api('mobileapi'), 'api.example.com', {
  proxy: 'http://127.0.0.1:8888'
});

Aliases Bluecat.ServiceSync and Bluecat.ServiceAsync both point at Service for backward-compatible imports.

setProxy(proxy) / setHeaders(headers) / getHeaders()

Manage per-instance proxy and fixed-header overrides at any time.

setSessionRules(rules)

Carry over an auth token automatically across requests:

service.setSessionRules({
  requestHeader:      'AUTH_TOKEN',
  responseHeader:     'REFRESH_AUTH_TOKEN',
  startSessionHeader: 'start-auth-token-value'
});

resetCookie()

Clear the in-memory cookie jar.

beforeRequest(fn) / afterResponse(fn)

Register hooks. Both can be async.

service.beforeRequest(async ctx => {
  ctx.request.headers['X-Trace-Id'] = await getTraceId();
});

service.afterResponse((ctx, result) => {
  if (result.data?.statusCode >= 500) console.error('5xx', ctx.request.uri);
});

sleep(ms)

Returns a promise that resolves after ms milliseconds.

Bluecat.Api(name, [apiPath], [urlCallback])

Loader for config/api.json. The optional urlCallback is an escape hatch when host/scheme rules cannot be expressed declaratively in the JSON tree:

// custom URL builder used by every request
const api = Bluecat.Api('mobileapi', target => {
  target.url = function (self) {
    // 'self' has { path, host, headers, schema, ... }
    return `https://${self.host || 'api.example.com'}/internal/${self.path}`;
  };
  return target;
});
const service = new Bluecat.Service(api, 'api.example.com');

TypeScript

Bluecat ships type declarations. The base Service is intentionally [key: string]: any because the method tree is generated from runtime JSON. Augment it with your own interface for first-class autocomplete:

import * as Bluecat from 'bluecat';

interface MyService extends Bluecat.Service {
  users: {
    GET(opts?: Bluecat.RequestOptions): Promise<Bluecat.RequestResult>;
    '${id}': {
      GET(opts: Bluecat.RequestOptions & { params: { id: string } }):
        Promise<Bluecat.RequestResult>;
    };
  };
}

const service = new Bluecat.Service(
  Bluecat.Api('api'), 'api.example.com'
) as MyService;

const r = await service.users['${id}'].GET({ params: { id: '42' } });
//      ^? Bluecat.RequestResult

For projects already maintaining an OpenAPI spec, generators like openapi-typescript produce richer per-endpoint types — Bluecat is agnostic to where the types come from.


OpenAPI 3 adapter

Already maintain an OpenAPI spec? Skip writing api.json by hand:

const Bluecat = require('bluecat');
const spec = require('./openapi.json');

const tree = Bluecat.fromOpenAPI(spec);          // -> { api: { ... } }
const host = Bluecat.hostFromOpenAPI(spec);      // first server URL host
const service = new Bluecat.Service(tree.api, host);

OpenAPI path parameters are translated to Bluecat's ${name} URI template form, so the runtime API is the same as for a hand-written tree:

// OpenAPI:   /users/{id}
// Bluecat:   tree.api.users['${id}']
await service.users['${id}'].GET({ params: { id: '42' } });

Command line tool

bluecat config             # scaffold a new test framework
bluecat api                # list routes defined in ./config/api.json
bluecat openapi <spec>     # convert an OpenAPI 3 spec to ./config/api.json

bluecat openapi:

  • accepts JSON (.json) directly
  • accepts YAML (.yaml / .yml) if you also npm install yaml
  • always writes to ./config/api.json (creates the directory if missing)
  • prints the suggested host taken from the first servers[].url

config/api.json is validated on load. Errors point at the offending JSON path:

Error: Invalid api.json at "api.v1.users.method": "FETCH" is not a supported HTTP method (GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS)

Logging

Set one of these env vars and Bluecat appends every exchange:

  • BLUECAT_DEBUG_FILE=/path/to/bluecat.log — append to a file
  • BLUECAT_DEBUG_CONSOLE=true — print to stdout

Each entry is a JSON object preceded by a separator line, suitable for piping into jq:

//---------------------------------
{
    "request":  { "method": "GET", "uri": "...", "headers": { ... }, "body": ... },
    "response": { "statusCode": 200, "headers": { ... }, "payload": { ... } },
    "responseTime": 123
}

Migration from v1.x

| v1.x | v2.x | |-------------------------------------------------|-------------------------------------------------------| | service.run(fn) wrapping sync calls (fibers) | await service.foo.GET() directly — no wrapper needed | | var r = service.foo.GET() (sync via fibers) | const r = await service.foo.GET() (returns a Promise) | | request lib options (gzip: true, agent, …) | pass an undici.Dispatcher via options.dispatcher | | node >= 0.10.21 | node >= 18 |

service.run(fn) is preserved as a thin Promise.resolve().then(fn) wrapper so v1 test files migrate by sprinkling in async/await. New code should call methods directly.


License

Licensed under the MIT