the-owl
v2.0.0
Published
Generate interactive API docs from your functional tests
Readme
the-owl
Generate interactive HTML API docs from your functional tests — a browsable site where, when served live, each captured request is editable and fireable right from the page.
the-owl watches the request/response traffic of your Express app while its
functional tests run, then turns that traffic into a browsable docs site — no
hand-written .yml or @jsdoc to keep in sync. You mark the requests you care
about, run your tests, and the-owl build renders the result.
How it works
capture middleware → Collector → .owl/*.json → `the-owl build` → docs/site/- Connect the capture middleware to your Express app.
- Mark the requests you want documented in your tests (per request, opt-in).
- Run your tests with
CREATE_DOCS=true; each test process drains its captured Examples to.owl/*.json. - Build the docs:
the-owl buildmerges every.owl/*.jsoninto a single Catalog and writes the site todocs/site/.
Each captured request/response pair is an Example. Examples sharing the same
method + route form an Endpoint; the merge of all Endpoints is the
Catalog the docs app renders.
Install
npm install --save-dev the-owlQuickstart (Vitest)
This mirrors examples/01-minimal.
1. Connect the capture middleware (src/server.ts):
import express, { type Express } from "express";
import theOwl from "the-owl";
export const createApp = (): Express => {
const app = express();
theOwl.connect(app); // capture middleware — mount before body parsers/routes
app.use(express.json());
app.get("/health", (_req, res) => res.status(200).send("OK"));
app.get("/users", (_req, res) =>
res.status(200).json([{ id: 1, name: "John" }, { id: 2, name: "Paul" }]));
return app;
};2. Mark the requests you want documented and drain after the run
(test/health.test.ts):
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { Server } from "node:http";
import theOwl from "the-owl";
import { owlHeaders } from "the-owl/vitest";
import { startServer, stopServer } from "./helpers";
let server: Server;
let base: string;
beforeAll(async () => {
({ server, base } = await startServer());
});
afterAll(async () => {
theOwl.save(); // drain this file's Examples to .owl/
await stopServer(server);
});
describe("[get] /health", () => {
it("(200) returns the application status", async () => {
const res = await fetch(`${base}/health`, {
headers: { "your-custom-header": "appears in the docs", ...owlHeaders() },
});
expect(res.status).toBe(200);
expect(await res.text()).toBe("OK");
});
});Opt-in is per request. Only requests carrying
owlHeaders()(thex-test-nameheader) are captured — attach it to the specific request you want documented, never to a shared request helper. The test's title becomes the Example name.
3. Wire the pipeline in package.json:
{
"scripts": {
"test": "vitest run",
"test:create-docs": "rimraf .owl docs && CREATE_DOCS=true vitest run && the-owl build"
}
}save() only writes files when CREATE_DOCS is set, so your everyday npm test
(and watch mode) stays clean. Run npm run test:create-docs to generate the docs.
Two delivery modes
Both modes read the same the-owl build output:
- Static — open or host the generated
docs/site/directory directly (docs/site/index.html+ assets). Read-only, but every Example offers a Copy as cURL command you can run against your own API. Set the base URL baked into those commands with theTHE_OWL_DOCS_HOSTenv var at build time (e.g.THE_OWL_DOCS_HOST=https://api.example.com the-owl build); it defaults tohttp://localhost:3000. - Live route — mount
theOwl.docs()to serve the docs from your running app, e.g. only in staging:
import theOwl from "the-owl";
if (process.env.NODE_ENV === "staging") {
app.use("/docs", theOwl.docs());
}In this mode the docs are interactive: expand any Example, edit its path
params / query / headers / body, and click Try it out to fire a real
same-origin request and see the live response. The captured x-test-name
header is dropped automatically, and any redacted value is shown as an empty
field you must fill before firing. The static build has no server to call, so
it stays read-only.
Both modes also expose a Copy as cURL button on each Example. In live mode
the command reflects your current edits and targets the page's own origin; in
static mode it uses the THE_OWL_DOCS_HOST base URL. Redacted or missing
values appear as <CHANGE_ME:name> so it's obvious what to fill in before
running the command.
Either way, the docs page carries a left navigation that lists every endpoint as a collapsible group of its Examples. Selecting one deep-links to that Example via the URL hash — expanding and scrolling to it — so you can share a link straight to a single Example, and reloading that URL reopens it. On narrow screens the navigation collapses behind a ☰ menu.
Secret redaction
Captured traffic is sanitized before it touches disk. By default, sensitive
header values (authorization, cookie, set-cookie, proxy-authorization)
and body/query keys (password, token, secret, …) are masked with
«redacted» — the key is kept, the value is replaced. Customize via
connect(app, options). See the API docs.
Environment variables
the-owl reads a few environment variables — one while your tests run, the rest
at the-owl build time (one is also read by the live theOwl.docs() router).
See examples/02-elaborate for THE_OWL_DOCS_HOST and
THE_OWL_GROUPING_REGEX wired into its test:create-docs scripts, and
examples/03-site-output-dir for
THE_OWL_SITE_OUTPUT_DIR.
| Variable | Read at | Purpose | Example |
| --- | --- | --- | --- |
| CREATE_DOCS | test run | Gate for save(): when unset, draining is a no-op, so everyday vitest run and watch mode never write to .owl/. Set it only on the run that (re)generates docs. | CREATE_DOCS=true vitest run |
| THE_OWL_DOCS_HOST | the-owl build | Base URL baked into the static build's Copy as cURL commands. Defaults to http://localhost:3000. | THE_OWL_DOCS_HOST=https://api.example.com the-owl build |
| THE_OWL_GROUPING_REGEX | the-owl build | Strips the longest matching leading path prefix before the sidebar groups Endpoints, so grouping falls on the next segment (^/api$ groups /api/v1/… under V1). When unset, Endpoints group by their first path segment. | THE_OWL_GROUPING_REGEX=^/api$ the-owl build |
| THE_OWL_SITE_OUTPUT_DIR | the-owl build & live theOwl.docs() | Full directory the static site is written to (no site/ suffix appended). Relative paths resolve against the cwd; absolute paths are used as-is. Defaults to docs/site. The live docs() router reads it to locate catalog.json too. | THE_OWL_SITE_OUTPUT_DIR=public/api-docs the-owl build |
THE_OWL_DOCSandPORTinexamples/02-elaborateare that example app's own conventions (whether to mounttheOwl.docs(), and which port to listen on) — not variablesthe-owlitself reads.
Motivation
Enforce functional-test development by earning something tangible from it.
API contracts usually change in code while the documentation rots, because it
lives in a separate .yml or @jsdoc that developers forget to update. the-owl
was built on the idea that all changes should be made in code — write the
tests, get the docs.
Documentation
Please see the files in the /documentation directory:
Contributing
Please refer to this document.
