jerant
v0.2.0
Published
Zero-config automatic API documentation for Express, Fastify, Koa and Hono. OpenAPI 3.1 + Swagger UI/Redoc/Scalar served from your own app at /docs.
Maintainers
Readme
jerant
Zero-config automatic API documentation for Express, Fastify, Koa and Hono.
FastAPI-style docs — OpenAPI 3.1 + a modern dark UI — served from your own app, on the same port, with no annotations required. Routes are discovered automatically, schemas are learned from your validation libraries, your Prisma schema, your source code, and real traffic.
npm install jerantconst express = require("express");
const { jerant } = require("jerant");
const app = express();
jerant(app);
app.listen(3000);That's it:
| URL | What |
| --- | --- |
| http://localhost:3000 | Your application, unchanged |
| http://localhost:3000/docs | Interactive docs (dark purple UI, search, try-it-out, teach mode) |
| http://localhost:3000/openapi.json | OpenAPI 3.1 document |
No separate documentation server, no code generation step, no YAML.
Table of contents
- How your API gets documented
- Confidence scores & teach mode
- Validation libraries
- Prisma-aware tracing
- Authentication & the Authorize button
- Custom validation errors
- Response envelopes & default responses
- Example values
- Tag groups
- The docs UI
- CLI: export & breaking-change guard
- Schema drift detection
- File uploads / form-data
- Data safety & redaction
- Storage & persistence
- Fastify, Koa and Hono
- Full configuration reference
- Programmatic API
- Production notes
- Architecture
1. How your API gets documented
jerant picks the highest-quality schema source available for every route, automatically:
| Priority | Source | Confidence | What it needs from you |
|---|---|---|---|
| 1 | Validation schemas — Zod, Joi, Yup, AJV, plain JSON Schema, class-validator | 100% | a validate({...}) on the route |
| 1 | express-validator chains, class-validator classes | 90% | nothing — detected automatically |
| 2 | Runtime traffic analysis — real requests/responses observed live | 40–90% | just use the API |
| 3 | Prisma schema tracing — prisma.user.create({data: req.body}) + schema.prisma | 75% | nothing |
| 4 | Static code analysis — fields your handlers read (req.body.x, {a} = req.body), status codes, upload config; includes a full project source scan that sees through wrappers, named controllers, and follows payloads down to SQL/ORM writes | 55% | nothing |
| 5 | Route scanning — every route (nested routers included) appears from the moment the server starts | 25% | nothing |
Every operation shows its source and confidence in the UI and carries machine-readable metadata in x-jerant (source, confidence, observation count, drift issues).
2. Confidence scores & teach mode
Raising a score is always the same move: give jerant a better source.
- Add
validate({...})→ instant 100% - Handler writes into a Prisma model → 75% automatically
- Send real requests → climbs to ~90% (saturates around 30 observations)
Teach mode closes the loop from inside the docs. Any operation below 50% confidence opens automatically with a "◌ Teach this endpoint" panel where anyone can freely enter:
- path params — one input per
{param} - query params — unlimited key/value rows (no declaration needed)
- headers — e.g. an
Authorizationtoken - body — raw JSON, form-data (with real file uploads), or url-encoded
Hitting Send request exercises your real app; the analyzer observes the request and response, the confidence chip updates live, and a reload shows the learned schema and parameters. Once an operation crosses 50% it graduates to a normal documented endpoint. Disable with teachMode: false.
3. Validation libraries
Zod (recommended)
validate() gives you real request validation and perfect docs from one declaration:
const { z } = require("zod");
const { jerant, validate } = require("jerant");
app.post(
"/users",
validate({
summary: "Create a user",
tags: ["users"],
body: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().optional(),
}),
responses: { 201: z.object({ id: z.number(), name: z.string() }) },
}),
(req, res) => res.status(201).json({ id: 1, name: req.body.name })
);Invalid requests get a structured error response; the docs get exact schemas. Zod v3 and v4 are both supported (including z.iso.date(), z.looseObject(), discriminated unions).
Joi, Yup, AJV, JSON Schema, class-validator
validate() accepts them all — the library is detected automatically:
validate({ body: Joi.object({ sku: Joi.string().required() }) })
validate({ body: yup.object({ email: yup.string().email().required() }) })
validate({ body: ajv.compile(schema) }) // compiled AJV validator
validate({ body: { type: "object", properties: { id: { type: "integer" } } } })express-validator
Detected with no jerant code at all:
app.post("/subscribe", body("email").isEmail(), handler);
// documented as { email: string (format: email), required }Documentation without validation
doc() attaches metadata and schemas without enforcing anything:
const { doc } = require("jerant");
app.get("/reports", doc({ summary: "List reports", tags: ["reports"] }), handler);4. Prisma-aware tracing
When a handler — or the service function it calls — writes request data into a Prisma model, jerant reads schema.prisma and types the body fields from the model:
app.post("/users", async (req, res) => {
const user = await prisma.user.create({ data: req.body });
res.status(201).json(user);
});
// documented from schema.prisma: email (string, required), name (string,
// required), age (integer), role (enum ADMIN|MEMBER)Server-managed columns are excluded automatically: @id, @updatedAt, and generated defaults (now(), autoincrement(), uuid()). Fields with literal defaults (@default(MEMBER)) are documented as optional. Configure with prisma: { schemaPath: "db/schema.prisma" }, disable with prisma: { enabled: false }.
5. Authentication & the Authorize button
Declare your auth model once — every operation documents it and Swagger UI shows its Authorize button:
jerant(app, { security: "bearer" }); // or "apiKey", "basic", "cookie"Mark exceptions per route (security: [] = public):
app.post("/login", validate({ security: [] }), loginHandler);Full control:
jerant(app, {
security: {
schemes: {
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
apiKeyAuth: { type: "apiKey", in: "header", name: "X-Service-Key" },
},
default: "bearerAuth",
},
});With no configuration at all, jerant auto-detects auth middleware from its source — req.headers.authorization reads, Bearer checks, passport.authenticate(...), x-api-key lookups, or middleware named requireAuth/isAuthenticated/etc. — and documents those routes as secured. Disable with security: false or security: { autoDetect: false }.
6. Custom validation errors
The default failure response is 400 {error, details}. Reshape it app-wide:
jerant(app, {
validationError: {
status: 422,
formatter: ({ details }) => ({ success: false, errors: details }),
},
});…or per route: validate({ body: schema, error: { status: 422 } }).
7. Response envelopes & default responses
If every response ships in a wrapper, declare it once — declared response schemas slot into $data automatically:
jerant(app, {
envelope: { success: "boolean", message: "string", data: "$data" },
defaultResponses: {
401: { description: "Not signed in", schema: { type: "object", properties: { error: { type: "string" } } } },
500: { description: "Internal error" },
},
});defaultResponses are added to every operation that doesn't already document that status. The envelope also accepts a full JSON Schema containing a "$data" slot, plus { schema, statuses: [200, 201] } to control which statuses get wrapped (default: all 2xx).
8. Example values
Every request/response body carries an example whose values are the type names — instant, self-explanatory, and guaranteed to never leak data:
{ "email": "email", "password": "string", "age": "number", "score": "float" }| examples option | Behavior |
|---|---|
| "placeholder" (default) | type-name values as above |
| "captured" | real (redacted) traffic payloads |
| false | no examples |
9. Tag groups
Give the sidebar groups descriptions and a fixed order:
jerant(app, {
tags: [
{ name: "users", description: "User management" },
{ name: "orders", description: "Order processing" },
"internal",
],
});Routes without explicit tags are grouped by their first meaningful path segment (/api/v1/users/… → users).
10. The docs UI
The default UI is a modern dark theme (deep purple surfaces, lavender accents) with:
- a stats header: endpoints, tag groups, schemas, average confidence, drift issues, and endpoints still in learning mode
- per-operation confidence chips — color-graded score with source, observation count and drift warnings on hover
- Expand all / Collapse all, Copy spec URL, Download JSON buttons
/keyboard shortcut to jump to the endpoint search- teach panels on low-confidence operations (see §2)
- validated color palette (contrast + colorblind-safe method badges on the dark surface)
Choosing a UI
jerant(app, { ui: "swagger" }); // default
jerant(app, { ui: "redoc" }); // Redoc, same dark purple theme
jerant(app, { ui: "scalar" }); // Scalar, purple dark modeSelf-hosting the assets (offline / strict CSP)
npm install swagger-ui-distjerant(app, { assets: "local" }); // served at /docs/assets/*, allowlistedBring your own UI
jerant(app, { ui: false }); // serve only /openapi.json
app.get("/mydocs", (req, res) => res.sendFile("my-ui.html"));
jerant(app, { ui: { html: myHtmlString } }); // your page at docsPath
jerant(app, { // or a render function
ui: ({ openapiPath, title }) => `<html>… fetch("${openapiPath}") …</html>`,
});Any custom UI gets the full spec including the x-jerant metadata (source, confidence, drift), so you can render your own badges.
11. CLI: export & breaking-change guard
npx jerant export server.js -o openapi.json # CI / codegen / Postman
npx jerant diff openapi.main.json openapi.json # exits 1 on breaking changes
npx jerant diff main-server.js server.js # app entries work tooexportloads your app (withJERANT_EXPORT=1set, so you can skiplisten()if you want) and writes the full OpenAPI document.--wait <ms>allows extra time for async route registration;--prettyformats stdout output.diffreports breaking changes — removed operations, new required request fields/params, type changes, removed response properties — and exits1when any are found (--fail-on anyalso fails on additions). Wire it into CI to block accidental breakage.
12. Schema drift detection
When a route has a declared schema and enough observed traffic (≥5 requests), jerant compares the two and flags mismatches:
- type-mismatch — declared
number, traffic showsstring - required-field-never-observed — declared required, absent from every payload
- undeclared-field — appears in traffic but the schema forbids extras
Findings land in x-jerant.drift per operation with a spec-level summary; drifting operations show a ⚠ on their confidence chip. Disable with driftDetection: false.
13. File uploads / form-data
jerant documents the correct request media type per route:
- Routes with upload middleware (multer, express-fileupload) are documented as
multipart/form-dataautomatically; the source scan recovers real field names from your config (upload.single('avatar'),upload.fields([{ name: 'id_front' }])). - File fields seen at runtime (
req.file/req.files) appear as binary inputs alongside schema fields. - Explicit control when you want it:
app.post("/avatars",
upload.single("avatar"),
doc({ contentType: "multipart/form-data", files: ["avatar"] }),
handler
);14. Data safety & redaction
jerant never stores authorization headers, cookies, or session identifiers (headers are not captured at all), and automatically redacts sensitive fields from stored payloads and examples — password, token, secret, apiKey, credit-card fields, and anything that looks like a credential (JWTs, Bearer … values):
{ "password": "***REDACTED***" }Add your own with redactKeys: ["internal_code"]. With the default examples: "placeholder", stored payloads never surface in the docs at all.
15. Storage & persistence
Learned schemas live in memory by default. To survive restarts:
jerant(app, { storage: { mode: "file", path: ".jerant/data.json" } });Multiple instances behind a load balancer should share a store instead — anything with load()/save() works (both may be async):
jerant(app, {
storage: {
mode: "custom",
store: {
load: async () => JSON.parse(await redis.get("jerant") || "null"),
save: async (data) => redis.set("jerant", JSON.stringify(data)),
},
},
});Writes are debounced and atomic (file mode), with a flush on process exit.
16. Fastify, Koa and Hono
The core is framework-agnostic; adapters ship in the box and provide the same docs, runtime learning, and UI:
// Fastify — route schemas become first-class docs (call before routes)
const { jerantFastify } = require("jerant");
jerantFastify(fastify);
// Koa — pass your @koa/router instance(s)
const { jerantKoa } = require("jerant");
jerantKoa(app, { routers: [router] });
// Hono — call before registering routes
const { jerantHono } = require("jerant");
jerantHono(app);17. Full configuration reference
Everything is optional:
jerant(app, {
// Identity (defaults come from your package.json)
title: "My API",
version: "2.0.0",
description: "…",
servers: [{ url: "https://api.example.com" }],
// Paths
docsPath: "/docs",
openapiPath: "/openapi.json",
// UI
ui: "swagger", // "redoc" | "scalar" | false | {html} | (ctx) => html
assets: "cdn", // "local" = self-hosted swagger-ui-dist
cdn: "https://unpkg.com/swagger-ui-dist@5",
// Documentation behavior
security: "bearer", // auth modeling + Authorize button (§5)
validationError: { status: 422 },// validate() failure shape (§6)
envelope: { success: "boolean", data: "$data" }, // §7
defaultResponses: { 500: { description: "Internal error" } },
tags: [{ name: "users", description: "User management" }],
examples: "placeholder", // "captured" | false (§8)
teachMode: true, // open low-confidence ops for learning (§2)
driftDetection: true, // §12
// Learning engine
runtime: {
enabled: true, // runtime traffic analysis
sampleRate: 1, // sample a fraction of requests
maxExamples: 3, // stored examples per route/status
maxObservations: 1000, // merge cap per route
maxBodyBytes: 102400, // skip larger payloads
},
sourceScan: { enabled: true }, // project source analysis
prisma: { enabled: true }, // schema.prisma tracing (§4)
// Data safety
redactKeys: ["internal_id"],
// Persistence (§15)
storage: { mode: "memory" }, // "file" | "custom"
});Disable entirely with enabled: false or the JERANT_DISABLED=1 environment variable.
18. Programmatic API
const docs = jerant(app);
docs.getSpec(); // current OpenAPI document (cached)
docs.refresh(); // force a rebuild
docs.flush(); // persist learned schemas now
docs.close(); // stop persistence timers19. Production notes
- Overhead: the middleware only wraps
res.json/res.send(restored after each response) and defers all analysis tosetImmediateafter the response is sent — nothing blocks the request path. Useruntime.sampleRateto sample under heavy load. - Call order: call
jerant(app)right afterexpress()(before your body parser and routes) so the analyzer sees every response. Route registration order doesn't matter — discovery is lazy. - CSP: the
/docspage loads UI assets from a CDN by default and uses an inline script. Under a strict Content-Security-Policy useassets: "local"or allow thecdnorigin. - CI:
npx jerant export+npx jerant diffgive you spec artifacts and a breaking-change gate without booting a server in tests.
20. Architecture
Express / Fastify / Koa / Hono App
│
Route Scanner ──► discovers every route & nested router
│
├── Validation Adapters (Zod / Joi / Yup / AJV / express-validator / class-validator)
├── Prisma Schema Tracing (schema.prisma → typed body fields)
└── Runtime Analyzer ──► Schema Inference Engine (+ redaction, confidence, drift)
│
OpenAPI 3.1 Generator (security, envelopes, tags, default responses, teach mode)
│
├── /openapi.json (also: npx jerant export / diff)
└── /docs (dark UI: Swagger / Redoc / Scalar / your own)Schema adapters and framework adapters both operate on plain instances — nothing in jerant imports a web framework.
Examples
node examples/zod-app.js # modern app with Zod validation
node examples/legacy-app.js # legacy app, docs learned from trafficRoadmap
- Done: Express/Fastify/Koa/Hono · all major validation libraries · runtime learning + teach mode · security modeling · envelopes & default responses · drift detection · Prisma tracing · export/diff CLI · pluggable storage · dark UI with Swagger/Redoc/Scalar/custom
- Next: NestJS adapter, TypeScript metadata, SDK generation
License
MIT
