inkan

Typed client

A client typed from the app itself, without code generation.

The contract already knows every path, input and answer, so a client can take its types from the app itself. No code generation, no OpenAPI step in between:

// server.ts: define the routes in a chain, so the type of `api` sees them
export const  = ()
  .("/teas/:id", { : .({ : .() }), : { 200: Tea, 404: .() } }, getTea)
  .("/teas", { : NewTea, : { 201: Tea } }, addTea)
  .("/admin", admin);

// web.ts: only the type crosses over, none of the server
import type {  } from "./server.ts";
import {  } from "@vxnsin/inkan/client";

const  = <typeof >("https://shop.example.com", { : () => ({ : `Bearer ${token}` }) });

const  = await .get("/teas/:id", { : { : 1 } });
if (.ok) .data.name;               // typed as the contract's Tea, Dates as strings
else if (.status === 404) .problem; // every failure is a problem document, never a throw

A wrong path, a missing param or a body of the wrong shape does not compile. Check ok first: any status can also come from a proxy on the way, so status alone cannot promise which answer it is. The client is a few lines around fetch and runs anywhere fetch does; pass your own with { fetch }.

const  = await shop.get("/teas/:id", { : { : 1 }, : .(5000) });

Every call takes a signal: AbortSignal.timeout(ms), or a controller's to cancel it when the user moves on. New in 0.8.0.

Routes written one statement at a time (app.get(...);) work as always, but their type cannot collect them: a type only grows along a chain.

Events

for await (const  of shop.events("/exports/:id/events", { : {  } })) {
  if (.event === "progress") bar.set(.data.done / .data.total); // typed by the event's schema
  if (.event === "end") break;
}

events(path, { params, query, headers, signal }) reads a route that answers with t.events(...), as an async iterator. Each event is narrowed by its name, so e.data has the type of that event's schema. Leaving the loop, or aborting signal, closes the stream. A stream that does not open throws a StreamError with its status and its problem. Only routes with an events contract are offered as paths. New in 0.7.0; jobs use it to follow their progress.

WebSockets

const  = shop.ws("/rooms/:room", { : { : "tea" }, : { : "ana" } });
await .ready;
.send({ : "say", : "hello" });           // typed by the route's message
for await (const  of ) if (.type === "said") show(.text); // typed by its send

ws(path, { params, query, headers }) opens a route made with app.ws with the platform's WebSocket. Leaving a for await loop closes it; headers go with the upgrade only in Node. New in 0.7.0.

Live data in a component

import {  } from "@inkanjs/query";
import { ,  } from "@inkanjs/query/react";

const  = (shop);
const  = (.events("/teas/:id/events", { : {  } }));   // feed.last, feed.byName.brewed
const  = (.ws("/rooms/:room", { : {  } }));         // room.send(...), room.last

@inkanjs/query turns events and ws into React hooks that open on mount, open again when the path or input changes, and close on unmount, typed from the same contracts. follow and connect do the same without React, for Vue, Svelte or Solid.

Routes out of plugins

Routes a plugin defines in its chain and returns with its scope reach the client after register, under their prefix: see Writing a plugin. New in 0.7.0.

On this page