Skip to content

Server Adapters

TypoKit is not an HTTP server. It is a typed application framework that compiles your route contracts, validators, and middleware into a portable route table — then delegates actual HTTP handling to a server adapter.

This separation means you can swap HTTP frameworks without changing a single handler, middleware, or contract.

Understanding what TypoKit owns versus what the adapter owns is key to using the system effectively.

ConcernOwnerDetails
Request validationTypoKitCompiled Typia validators run on every request
Response serializationTypoKitfast-json-stringify for typed output
Typed middleware pipelineTypoKitdefineMiddleware with context type-narrowing
Context creationTypoKitctx.log, ctx.fail(), ctx.user, ctx.requestId
Error handlingTypoKitAppError → structured ErrorResponse
Request lifecycle tracingTypoKitOpenTelemetry spans per request
Debug sidecar integrationTypoKitDev-mode diagnostics
Compiled route tableTypoKitPortable radix-tree data structure
HTTP parsing & connectionsAdapterKeep-alive, chunked encoding, etc.
Route registrationAdapterTranslates compiled route table → framework-native routes
Platform-specific optimizationsAdapterBun.serve(), Deno.serve(), etc.
Framework-native middlewareAdapterFastify plugins, Hono middleware, Express middleware

Every request flows through the same pipeline regardless of which adapter you choose:

Incoming HTTP Request
┌────────▼─────────────┐
│ Server Adapter │
│ │
│ 1. HTTP parsing │
│ 2. Framework-native │
│ middleware (CORS, │
│ compression, etc) │
│ 3. Normalize request │
│ → TypoKitRequest │
└────────┬─────────────┘
┌────────▼─────────────┐
│ TypoKit Core │
│ │
│ 4. Typed middleware │
│ chain (sorted by │
│ priority) │
│ 5. Request │
│ validation │
│ 6. Handler execution │
│ 7. Response │
│ serialization │
│ 8. Error handling │
│ 9. OTel span close │
└────────┬─────────────┘
┌────────▼─────────────┐
│ Server Adapter │
│ │
│ 10. Write HTTP │
│ response │
└──────────────────────┘

Every adapter implements this interface from @typokit/core:

export interface ServerAdapter {
name: string;
// Translate compiled route table into framework-native routes
registerRoutes(
routeTable: CompiledRouteTable,
handlerMap: HandlerMap,
middlewareChain: MiddlewareChain,
validatorMap?: ValidatorMap,
serializerMap?: SerializerMap,
): void;
// Start the HTTP server
listen(port: number): Promise<ServerHandle>;
// Convert framework-native request → TypoKitRequest
normalizeRequest(raw: unknown): TypoKitRequest;
// Convert TypoKitResponse → framework-native response
writeResponse(raw: unknown, response: TypoKitResponse): void;
// Escape hatch for framework-native plugins/server instance
getNativeServer?(): unknown;
}

The registerRoutes method receives the compiled route table — a portable radix-tree data structure — and translates it into whatever the underlying framework needs. The normalizeRequest and writeResponse methods handle the conversion between framework-specific types and TypoKit’s normalized types.

The default adapter. Zero external dependencies. Uses the compiled radix tree directly for O(k) route lookup where k is the number of path segments.

import { createApp } from '@typokit/core';
import { NativeAdapter } from '@typokit/server-native';
const app = createApp({
adapter: new NativeAdapter(),
});
app.register(userRoutes);
app.register(postRoutes);
await app.listen(3000);

Best for: New projects, simple APIs, maximum performance, minimal dependency footprint.

Integrates with an existing or new Fastify instance. Walks the compiled route table and registers each route with Fastify’s own router, then hands off to TypoKit’s pipeline for validation and handling.

import { createApp } from '@typokit/core';
import { FastifyAdapter } from '@typokit/server-fastify';
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
// Use Fastify plugins as usual
await fastify.register(import('@fastify/cors'), { origin: '*' });
await fastify.register(import('@fastify/compress'));
const app = createApp({
adapter: new FastifyAdapter(fastify),
});
app.register(userRoutes);
await app.listen(3000);

Best for: Teams already using Fastify, or needing Fastify’s plugin ecosystem.

Runs anywhere Hono runs — Node.js, Bun, Deno, and Cloudflare Workers.

import { createApp } from '@typokit/core';
import { HonoAdapter } from '@typokit/server-hono';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
const hono = new Hono();
hono.use('*', cors());
const app = createApp({
adapter: new HonoAdapter(hono),
});
app.register(userRoutes);
await app.listen(3000);

Best for: Multi-runtime deployments, edge computing, Cloudflare Workers.

Migration path for teams moving from Express to TypoKit.

import { createApp } from '@typokit/core';
import { ExpressAdapter } from '@typokit/server-express';
import express from 'express';
import cors from 'cors';
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.json());
const app = createApp({
adapter: new ExpressAdapter(expressApp),
});
app.register(userRoutes);
await app.listen(3000);

Best for: Incremental migration from Express, teams with existing Express middleware they need to keep.

Use the native adapter when:

  • Starting a new project from scratch
  • You want the smallest possible dependency tree
  • You don’t need framework-specific plugins
  • Maximum performance matters (direct radix-tree lookup, no framework overhead)

Use a framework adapter when:

  • You have an existing codebase using that framework
  • You need framework-specific plugins (e.g., Fastify’s schema-based serialization, Express session middleware)
  • You’re incrementally adopting TypoKit — mount TypoKit routes alongside existing framework routes
  • Your deployment target requires it (e.g., Hono for Cloudflare Workers)

Platform adapters and server adapters are orthogonal concerns. Platform adapters handle JavaScript runtime differences (Node.js, Bun, Deno). Server adapters handle HTTP framework choice. You choose both independently.

Node.jsBunDeno
TypoKit Native
Fastify✅ (compat)
Hono
Express✅ (compat)

Platform adapter packages:

  • @typokit/platform-node — Uses Node.js http / http2 modules
  • @typokit/platform-bun — Uses Bun.serve() for maximum Bun performance
  • @typokit/platform-deno — Uses Deno.serve() for native Deno support
import { createApp } from '@typokit/core';
import { NativeAdapter } from '@typokit/server-native';
import { BunPlatform } from '@typokit/platform-bun';
const app = createApp({
adapter: new NativeAdapter(),
platform: new BunPlatform(),
});
await app.listen(3000);

When no platform is specified, TypoKit auto-detects the current runtime.

All adapters provide an optional getNativeServer() method that returns the underlying framework instance. Use this when you need direct access to framework-specific APIs:

const app = createApp({
adapter: new FastifyAdapter(fastify),
});
// Access the raw Fastify instance
const nativeFastify = app.adapter.getNativeServer();
nativeFastify.addHook('onClose', async () => {
await db.close();
});