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

jolly-http

v0.3.1

Published

Workflow-as-code HTTP tool: simplicity of httpie, speed of xh, sequential + parallel + watched modes from one .mjs file

Readme

jolly-http

Workflow-as-code HTTP tool built on jolly-coop.

Simplicity of httpie, speed of xh, plus a thing neither has: the same .mjs file is your debug script, your test, and your load scenario.

Install

npm install -g jolly-http

Requires Node.js ≥ 22. Also runs on Bun and Deno via the published npm package.

Three modes, one mental model

1. Ad-hoc (httpie-shaped)

jolly-http GET https://api.github.com/users/arijit-gogoi
jolly-http POST https://httpbin.org/post name=ari age:=30 Auth:tok
jolly-http PUT https://api/users/1 --json '{"name":"ari"}'

Body shorthand:

| Form | Effect | |-------------------|-----------------------------------------------| | key=value | JSON string field | | key:=value | JSON literal (number, bool, null, array, obj) | | Header:value | Request header | | key==value | Query parameter | | key@path | File upload (form field) |

2. Workflow file (sequential)

jolly-http run flow.mjs
// flow.mjs
import { request, assert, env } from "jolly-http"

export default async function (vu, signal) {
  const login = await request.POST(`${env.API}/login`, {
    json: { user: vu.id },
    signal,
    timeout: "5s",
  })
  assert(login.status === 200, "login failed")

  const { token } = await login.json()

  const me = await request.GET(`${env.API}/me`, {
    headers: { authorization: `Bearer ${token}` },
    signal,
  })
  assert(me.status === 200)

  return { ok: true }
}

3. Same workflow, under load

jolly-http run flow.mjs -c 50 -d 30s --out samples.ndjson

The same file. No rewriting, no separate load-test DSL. SIGINT propagates through the in-process load runner; per-request samples go to NDJSON.

Workflow API (frozen — permanent public surface)

default export: (vu: VuContext, signal: AbortSignal) => Promise<any>

vu:     { id: number, iteration: number, env: Readonly<Record<string,string>> }
signal: AbortSignal (from the scope — pass to every fetch)

import { request, assert, env, sleep } from "jolly-http"

request.GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS (url, init) → Response

init: {
  headers?: Record<string,string>
  json?: unknown                // sets body to JSON + content-type
  form?: Record<string,string>  // url-encoded
  body?: string | Uint8Array
  query?: Record<string, string | number | boolean>
  timeout?: string | number     // "5s", 1000
  signal?: AbortSignal          // composed with scope signal
  cookies?: boolean             // false → opt this request out of the jar
}

assert(cond, msg?)              // throws AssertionError when falsy
env.FOO                         // --env flags + process.env + .env files
sleep("200ms" | 200)            // signal-aware

Common options

--header, -H <k:v>     Add a header (repeatable)
--json <str>           Body as JSON string (overrides shorthand)
--form                 Send x-www-form-urlencoded
--timeout <dur>        Per-request timeout ("500ms", "30s", "2m")
--user-agent <str>     Override User-Agent
--quiet, -q            Suppress per-request output
--out <path>           Append NDJSON samples to path
--env KEY=VAL          Set workflow env var (repeatable)
--env-file <path>      Load env vars from a file (repeatable; later wins)
--no-env-file          Skip auto-loading ./.env
--require-env <path>   Fail-fast if any key from <path> is unset/empty
--cookies <dir>        Persist cookies as <dir>/vu-N.json (jar is always on)
--har <dir>            Record HAR as <dir>/vu-N.har
--har-replay <path>    Replay responses from a recorded HAR (file or dir)
--insecure, -k         (no-op; see "Self-signed certs" below)

Load mode:
  -c, --concurrency <n>  Virtual users
  -d, --duration <dur>   Total duration ("30s", "2m")
  --rps <n>              Target requests/sec
  --warmup <dur>         Exclude first N from stats

Watch mode (run only):
  --watch                Rerun workflow on file change
  --watch-mode <mode>    eager (cancel mid-flight, default) | lazy (queue)

Exit codes: 0 success · 1 fatal or assertion failure · 2 bad args · 130 SIGINT.

NDJSON schema

One line per HTTP request emitted by request.*:

{"ok":true,"t":0.142,"vu":7,"iteration":0,"method":"POST","url":"https://api/login","status":200,"duration_ms":38.2,"size":312,"ts":"2026-04-18T03:14:15.926Z"}
{"ok":false,"t":0.191,"vu":3,"iteration":1,"method":"GET","url":"https://api/me","duration_ms":501.1,"error":"AbortError","message":"request timed out after 500ms","ts":"2026-04-18T03:14:16.427Z"}

Same shape in single-run and load mode — any tool that reads one reads both.

Cookies

Cookies are on by default. Each workflow run gets a per-VU jar that auto-includes cookies on outbound requests and absorbs Set-Cookie headers from responses. A login → me-call workflow Just Works without configuration.

await request.GET(`${env.API}/login`)            // jar absorbs Set-Cookie
await request.GET(`${env.API}/me`)               // cookie sent automatically
await request.GET(`${env.API}/public`, { cookies: false })  // opt out per call

--cookies <dir> adds disk persistence (load on start, save on exit, including Ctrl-C):

jolly-http run flow.mjs --cookies ./jar
ls jar/    # → vu-0.json (per-VU files in load mode: vu-0.json, vu-1.json, …)

The jar implements RFC 6265 (Domain/Path/Secure/HttpOnly/Expires/Max-Age). It does not handle the public-suffix list or third-party cookie blocking — those are browser concerns.

Environment files

./.env is auto-loaded from cwd if present. Read values via the frozen env import:

const res = await request.GET(`${env.API_BASE}/users`, {
  headers: { authorization: `Bearer ${env.API_TOKEN}` },
  signal,
})

Precedence (highest wins)

--env KEY=VAL  >  process.env  >  --env-file files (later > earlier)  >  auto ./.env

Same as dotenv, Next.js, Vite, every modern framework.

jolly-http run flow.mjs --env-file .env.staging              # one file, no auto-load
jolly-http run flow.mjs --env-file .env --env-file .env.local # later overrides earlier
jolly-http run flow.mjs --no-env-file                        # skip ./.env

Explicit --env-file disables auto-loading ./.env.

Validation with --require-env

Pair a committed .env.example (placeholder values, in git) with a gitignored .env:

# .env.example          # .env (gitignored)
API_BASE=               API_BASE=https://api.example.com
API_TOKEN=              API_TOKEN=tok-xyz

jolly-http run flow.mjs --env-file .env --require-env .env.example

If any key listed in .env.example is unset or empty after the merge, the run fails fast before the workflow's first request, listing every missing key:

missing required env vars from .env.example:
  - API_TOKEN
set them in .env, export them, or pass --env KEY=VAL

Format

Standard dotenv dialect:

# Comment
KEY=value
QUOTED="value with spaces"
SINGLE='no $interpolation here'
INTERPOLATED=${KEY}/path
MULTILINE="line1
line2"
export FOO=bar     # bash-compat prefix; "export " is stripped

${VAR} interpolation only resolves against keys defined earlier in the same file. Bare $VAR is literal — no eating of $1, $@, currency strings.

Watch mode

Rerun on workflow file change:

jolly-http run flow.mjs --watch                       # eager (default)
jolly-http run flow.mjs --watch --watch-mode lazy     # queue, finish current first
  • eager — cancel in-flight requests and start a new run. Matches nodemon/vitest. Fast feedback.
  • lazy — queue file changes; current run finishes naturally, then reload.

Watch composes with load mode: jolly-http run flow.mjs --watch -c 50 -d 30s reruns the load on every change. Ctrl-C exits 130.

HAR recording

Capture full request/response pairs for inspection in DevTools or HAR viewers:

jolly-http run flow.mjs --har ./har-out
ls har-out/    # → vu-0.har (per-VU in load: vu-0.har, vu-1.har, …)

HAR 1.2 — opens in Chrome DevTools (Network → Import HAR), Firefox, or any HAR viewer. Bodies truncated to 64 KB.

Replay

Re-run a workflow against canned responses from a recorded HAR — useful for offline debugging, deterministic CI, or sharing fixtures:

jolly-http run flow.mjs --har ./fixtures              # record once
jolly-http run flow.mjs --har-replay ./fixtures       # replay (per-VU dir)
jolly-http run flow.mjs --har-replay ./fixtures/vu-0.har  # replay (single shared file)

Path auto-detects: *.har → shared file across VUs; directory → <dir>/vu-N.har per VU.

Matching is strict: (method, full URL with query, request body) must match an entry exactly. First-match-wins, no consume — workflows that loop over the same endpoint replay correctly. Misses throw HarReplayMissError (with .method, .url); CLI exits 1.

--har and --har-replay cannot both be set.

Caveats:

  • Form bodies (URLSearchParams) match by exact string — field order matters. Use json: for canonical ordering.
  • Headers are not part of the match (cookie drift between record/replay is tolerated).
  • Multipart bodies have non-deterministic boundaries; not reliably replayable.

Self-signed certs / internal CAs

To skip TLS validation, use your runtime's built-in flag — cross-runtime, zero-dep, properly scoped:

NODE_TLS_REJECT_UNAUTHORIZED=0 jolly-http run flow.mjs    # Node
bun --tls-no-verify run jolly-http run flow.mjs           # Bun
deno run --unsafely-ignore-certificate-errors jolly-http run flow.mjs  # Deno

Node prints a stderr warning — that's intentional UX. The proper fix for internal CAs is your system trust store; the runtime flags are an escape hatch for CI / dev iteration.

The --insecure, -k CLI flag is a no-op and may be removed in a future major.

Why .mjs?

Workflows are real JavaScript modules, not a DSL:

  • No parser divergence — if it runs in Node, it works.
  • Editor support is free — ESLint, Prettier, TypeScript JSDoc, go-to-definition all work.
  • Helpers compose. Write a retry wrapper, import it in three workflows.
  • Load and debug are the same file. No "production config" vs. "test script" drift.

Philosophy

Small surface, permanent contract. The workflow function signature ((vu, signal)) and the four runtime imports (request, assert, env, sleep) are the API. Everything else is implementation detail and can change.

Hurl and httpyac drowned in feature creep. jolly-http picks a different tradeoff: power comes from the .mjs file being real JavaScript, not from a thousand config options.

License

MIT — see LICENSE.