Introduction
An API framework for Node where the docs can't lie.
An inkan (印鑑) is the seal a Japanese contract gets stamped with. Here every route carries one: params, query, body, responses and a few examples, written once. From that one definition inkan checks what comes in, types your handler, checks what goes out, writes OpenAPI 3.1, serves the docs, and runs every example as a test.
import { , , } from "@vxnsin/inkan";
const = .({ : .(), : .(), : .(["green", "black", "oolong"]) });
const = [{ : 1, : "Sencha", : "green" as }];
const = ({ : "Tea Shop", : "1.0.0" });
.(
"/teas/:id",
{
: .({ : .() }),
: { 200: , 404: .() },
: [
{ : "found", : { : 1 }, : { : "Sencha" } },
{ : "missing", : { : 99 }, : 404 },
],
},
({ }) => {
const = .(() => . === .); // params.id is a number, not a string
if (!) throw (404, "tea-not-found", `There is no tea with id ${.}`);
return ; // has to be a Tea, or it does not compile
},
);
.();The examples are what the docs show, and they are what this runs. Say the 404 got lost in a refactor:
$ npx inkan check src/app.ts
印 inkan check · Tea Shop 1.0.0
GET /teas/:id
✓ found 200 0.8ms
✗ missing 200 0.4ms
answered 200, expected 404
2 examples · 1 sealed · 1 brokenA docs page that is tested is a docs page you can trust. When the handler drifts, the check goes red before anyone reads something false.
Why another one
Express gets out of the way, Fastify is fast, Hono runs everywhere. inkan wants to be easy to start and deep when you need it, plus the one thing none of them does: holding the server to what its docs say.
| Express | Fastify | Hono | inkan | |
|---|---|---|---|---|
| Handler types come from the schema | – | with a type provider | with a validator | ✓ |
| OpenAPI document | plugin | plugin | plugin | ✓ |
| Docs page | plugin | plugin | plugin | ✓ no CDN, works offline |
| Answers trimmed to the contract | – | ✓ | – | ✓ and checked in dev |
| Examples run as tests | – | – | – | ✓ inkan check |
| Live request inspector | – | – | – | ✓ /_inkan |
| Every error in one shape (RFC 9457) | – | – | – | ✓ |
| Credentials in the contract, refused with a 401 when missing | middleware | plugin | middleware | ✓ security, in OpenAPI too |
| Contracts as compiled code | – | at runtime, with new Function | – | ✓ ahead of time, inkan seal, no eval |
| Hooks, plugins, decorators | middleware | ✓ | middleware | ✓ |
| Bun, Deno, serverless | – | – | ✓ | ✓ app.fetch |
| A tutorial in the terminal | – | – | – | ✓ inkan learn |
| Runtime dependencies | several | several | none | none |
What it does
| Checks what comes in | params, query, headers and body are validated together. Path and query strings become the numbers and booleans the schema asks for. Bad input is one 400 that lists every issue, not just the first. |
| Types the handler | No generics to write. params.id is what the schema says, and so is the return value. Without a schema, :id in the path is still typed. |
| Checks what goes out | In development, an answer that breaks its contract is a 500 that says where, not a silent surprise for the frontend. Keys the contract does not list are dropped, so a passwordHash never leaves by accident. |
| Writes OpenAPI 3.1 | At /openapi.json, from the same schemas. Named schemas land in components once. |
| Serves the docs | At /docs. Every example has a send button, and a route gets its seal 印 when all of its examples answer as promised. |
| Runs examples as tests | inkan check or app.check(), in-process, no port. Routes without examples are listed, so nothing hides. |
| Shows what happened | /_inkan is a live log of the last 200 requests and what broke the contract. Development only, loopback only, secret headers hidden. |
| Errors in one shape | throw problem(404, "tea-not-found", "…") gives an RFC 9457 document. Every built-in error has the same shape, with a stable type to switch on. |
Stable from 0.8
From 0.8.0 the API is frozen: what is marked stable does not break in a minor release, and a
renamed name keeps working, with a warning in development, for at least two minors and six
months. Stability has the policy, what is still experimental, and the
names that changed in 0.8.0. State that has to outlive a request or a process, such as jobs,
rate limits and sessions, goes into one Store, in memory or shared through
Redis, PostgreSQL, MongoDB or MySQL.