inkan

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 broken

A 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.

ExpressFastifyHonoinkan
Handler types come from the schema–with a type providerwith a validator✓
OpenAPI documentpluginpluginplugin✓
Docs pagepluginpluginplugin✓ 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 missingmiddlewarepluginmiddleware✓ security, in OpenAPI too
Contracts as compiled code–at runtime, with new Function–✓ ahead of time, inkan seal, no eval
Hooks, plugins, decoratorsmiddleware✓middleware✓
Bun, Deno, serverless––✓✓ app.fetch
A tutorial in the terminal–––✓ inkan learn
Runtime dependenciesseveralseveralnonenone

What it does

Checks what comes inparams, 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 handlerNo 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 outIn 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.1At /openapi.json, from the same schemas. Named schemas land in components once.
Serves the docsAt /docs. Every example has a send button, and a route gets its seal 印 when all of its examples answer as promised.
Runs examples as testsinkan 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 shapethrow 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.

On this page