Migration from Express/Fastify
TypoKit is designed for incremental adoption. You don’t need to rewrite your existing Express or Fastify app — you install the matching server adapter, wrap your existing app instance, and start adding TypoKit routes alongside your current ones. Existing routes, middleware, and plugins keep working exactly as before.
What Changes vs. What Stays the Same
Section titled “What Changes vs. What Stays the Same”| Stays the Same | Changes (for new routes) |
|---|---|
| Existing routes and handlers | Request validation is automatic (compiled from types) |
| Framework-native middleware (CORS, helmet, compression) | Error handling uses structured AppError hierarchy |
| Database connections and ORM setup | Handlers receive fully typed HandlerInput |
| Authentication strategies | Response serialization uses fast-json-stringify |
| Static file serving, view engines | Middleware can narrow context types |
| Deployment and hosting setup | Route contracts define the API surface declaratively |
Your existing code runs untouched. New routes written with TypoKit get type safety, automatic validation, generated tests, and observability — but old routes are unaffected.
Migrating from Express
Section titled “Migrating from Express”-
Install the TypoKit Express adapter and CLI:
Terminal window pnpm add @typokit/core @typokit/server-express @typokit/errorspnpm add -D @typokit/cliTerminal window npm install @typokit/core @typokit/server-express @typokit/errorsnpm install -D @typokit/cli -
Wrap your existing Express app:
You likely have an
app.tsorindex.tsthat creates an Express instance. Pass it toexpressServer()so TypoKit registers its routes on the same app:src/app.ts import express from "express";import cors from "cors";import helmet from "helmet";import compression from "compression";import { createApp } from "@typokit/core";import { expressServer } from "@typokit/server-express";// Your existing Express app — nothing changes hereconst expressApp = express();expressApp.use(cors());expressApp.use(helmet());expressApp.use(compression());expressApp.use(express.json());// Your existing routes keep workingexpressApp.get("/health", (req, res) => {res.json({ status: "ok" });});expressApp.get("/legacy/users", (req, res) => {// ... your existing handler logicres.json({ users: [] });});// Wrap with TypoKit — new routes get type safety and validationconst app = createApp({server: expressServer({ app: expressApp }),routes: [// Add TypoKit route groups here as you migrate],});app.listen(3000); -
Add your first TypoKit route alongside existing ones:
Create a schema-first type and route contract for a new endpoint:
src/types/todo.ts /*** @table todos*/export interface Todo {/** @id @generated */id: string;title: string;completed: boolean;/** @generated */createdAt: Date;}export type CreateTodoInput = Omit<Todo, "id" | "createdAt">;src/routes/todos/contracts.ts import type { RouteContract } from "@typokit/core";import type { Todo, CreateTodoInput } from "../../types/todo.js";export const createTodo: RouteContract<void, void, CreateTodoInput, Todo> = {method: "POST",path: "/todos",};export const listTodos: RouteContract<void, void, void, Todo[]> = {method: "GET",path: "/todos",};src/routes/todos/handlers.ts import { defineHandlers } from "@typokit/core";import * as contracts from "./contracts.js";export default defineHandlers<typeof contracts>({createTodo: async ({ body, ctx }) => {// Your implementation hereconst todo = await db.insert("todos", body);return todo;},listTodos: async ({ ctx }) => {return await db.select("todos");},}); -
Register the new route group:
src/app.ts import express from "express";import cors from "cors";import { createApp } from "@typokit/core";import { expressServer } from "@typokit/server-express";import todosHandlers from "./routes/todos/handlers.js";import { logging } from "./middleware/logging.js";const expressApp = express();expressApp.use(cors());expressApp.use(express.json());// Existing routesexpressApp.get("/health", (req, res) => res.json({ status: "ok" }));const app = createApp({server: expressServer({ app: expressApp }),middleware: [logging],routes: [{ prefix: "/api", handlers: todosHandlers },],});app.listen(3000);Now
GET /healthis still handled by Express directly, whilePOST /api/todosandGET /api/todosgo through TypoKit’s typed pipeline with automatic validation. -
Migrate routes incrementally:
There’s no pressure to migrate everything at once. A typical migration timeline looks like:
- Week 1: Wrap existing app, add TypoKit to one new endpoint
- Week 2–4: Write new features as TypoKit routes, gaining test generation and validation
- Month 2+: Optionally migrate high-value existing routes to gain type safety
- Eventually: Remove legacy routes once they’ve been migrated (or keep them — it’s fine)
Migrating from Fastify
Section titled “Migrating from Fastify”-
Install the TypoKit Fastify adapter:
Terminal window pnpm add @typokit/core @typokit/server-fastify @typokit/errorspnpm add -D @typokit/cliTerminal window npm install @typokit/core @typokit/server-fastify @typokit/errorsnpm install -D @typokit/cli -
Create a TypoKit app with the Fastify adapter:
Unlike Express, Fastify’s plugin system means you configure options through the adapter and then register your existing plugins on the native instance:
src/app.ts import { createApp } from "@typokit/core";import { fastifyServer } from "@typokit/server-fastify";import fastifyCors from "@fastify/cors";import fastifyHelmet from "@fastify/helmet";import fastifyCompress from "@fastify/compress";import type { FastifyInstance } from "fastify";const app = createApp({server: fastifyServer({ logger: true, trustProxy: true }),routes: [// Add TypoKit route groups here],});// Access the native Fastify instance for existing pluginsconst fastify = app.getNativeServer() as FastifyInstance;// Your existing Fastify plugins keep workingfastify.register(fastifyCors, { origin: true });fastify.register(fastifyHelmet);fastify.register(fastifyCompress);// Your existing Fastify routes keep workingfastify.get("/health", async () => {return { status: "ok" };});fastify.get("/legacy/users", async (request, reply) => {// ... your existing handler logicreturn { users: [] };});app.listen(3000); -
Add TypoKit routes alongside existing ones:
Define typed contracts and handlers the same way as the Express example above:
src/app.ts import { createApp } from "@typokit/core";import { fastifyServer } from "@typokit/server-fastify";import todosHandlers from "./routes/todos/handlers.js";import { requireAuth } from "./middleware/auth.js";import type { FastifyInstance } from "fastify";const app = createApp({server: fastifyServer({ logger: true }),routes: [{prefix: "/api",handlers: todosHandlers,middleware: [requireAuth],},],});// Existing Fastify routes and pluginsconst fastify = app.getNativeServer() as FastifyInstance;fastify.register(fastifyCors);fastify.get("/health", async () => ({ status: "ok" }));app.listen(3000); -
Migrate routes incrementally:
The same incremental approach applies. Existing Fastify routes registered via the native instance work alongside TypoKit route groups. Migrate routes to TypoKit contracts when you want automatic validation, generated tests, or typed middleware.
How Middleware Coexists
Section titled “How Middleware Coexists”Understanding the request processing order is key to a smooth migration:
┌─────────────────────────────────────────────────────┐│ 1. HTTP connection received ││ 2. Framework-native middleware runs ││ (CORS, helmet, compression, rate limiting) ││ 3. Route matching ││ ├── Legacy route? → Framework handles directly ││ └── TypoKit route? → Continue to step 4 ││ 4. Request normalization → TypoKitRequest ││ 5. TypoKit middleware chain ││ (auth, logging, context enrichment) ││ 6. Compiled validators (params, query, body) ││ 7. Handler execution ││ 8. Response serialization ││ 9. Response writing │└─────────────────────────────────────────────────────┘Express Middleware
Section titled “Express Middleware”Your existing Express middleware keeps its registration order and runs before TypoKit sees the request:
// These run first, for ALL requests (legacy and TypoKit routes)expressApp.use(cors());expressApp.use(helmet());expressApp.use(compression());expressApp.use(express.json());
// TypoKit's typed middleware runs only for TypoKit routes, after normalizationconst app = createApp({ server: expressServer({ app: expressApp }), middleware: [logging, requireAuth], // Typed, context-narrowing middleware routes: [/* ... */],});Fastify Plugins
Section titled “Fastify Plugins”Fastify plugins registered on the native instance run in Fastify’s encapsulation order:
const fastify = app.getNativeServer() as FastifyInstance;
// These run first, for ALL requestsfastify.register(fastifyCors);fastify.register(fastifyRateLimit, { max: 100 });fastify.register(fastifyCompress);
// TypoKit middleware runs only for TypoKit routes, after normalizationWhat About Error Handling?
Section titled “What About Error Handling?”For TypoKit routes, errors are handled by TypoKit’s built-in error middleware:
AppErrorsubclasses produce structured JSON responses with appropriate status codes- Typia validation errors are formatted with field-level details
- Unknown errors return
500 Internal Server Errorin production
For legacy routes, your existing error handlers continue to work as before. TypoKit does not interfere with framework-native error handling for routes it doesn’t own.
Migration Checklist
Section titled “Migration Checklist”Use this checklist as you migrate:
- Install
@typokit/core, the matching server adapter, and@typokit/cli - Wrap your existing app (Express) or create the adapter (Fastify)
- Verify existing routes and middleware still work
- Define your first type with JSDoc annotations
- Create a route contract and handler
- Register the route group in
createApp() - Run
typokit generateto produce validators and DB schema - Run
typokit test:contractsto get auto-generated tests for free - Add more routes as TypoKit route groups over time
- Optionally migrate legacy routes when convenient
Frequently Asked Questions
Section titled “Frequently Asked Questions”Can I use both Express/Fastify routes and TypoKit routes on the same paths?
Section titled “Can I use both Express/Fastify routes and TypoKit routes on the same paths?”TypoKit routes are registered with a prefix (e.g., /api), so they don’t conflict with legacy routes unless you intentionally register them at the same path. If there’s a conflict, the framework’s route registration order determines which handler runs.
Do I need to change my database setup?
Section titled “Do I need to change my database setup?”No. TypoKit’s database adapters are optional — they generate schema files from your types, but you’re not required to use them. You can continue using your existing ORM, query builder, or raw SQL for both legacy and new routes.
What about authentication?
Section titled “What about authentication?”Your existing auth middleware (Passport, custom JWT verification, etc.) still runs at the framework level. For TypoKit routes, you can also create a typed defineMiddleware that narrows the context with user information — see the Middleware and Context page for details.
Can I eventually remove the framework adapter?
Section titled “Can I eventually remove the framework adapter?”Yes. Once all routes are migrated to TypoKit contracts, you can switch from expressServer() or fastifyServer() to the nativeServer() adapter for maximum performance and zero framework dependencies. The TypoKit route definitions don’t change — only the server adapter in createApp().
Next Steps
Section titled “Next Steps”- Server Adapters — understand the adapter architecture in depth
- Middleware and Context — learn how typed middleware works
- Error Handling — see how
ctx.fail()andAppErrorwork - Building Your First API — full tutorial for new TypoKit routes