Skip to content

Project Structure

After running typokit scaffold init, you’ll have a single-package project with a clean src/ layout and a generated .typokit/ directory. This guide explains what each piece does and which files you should edit.

A typical TypoKit project looks like this:

my-app/
├── typokit.config.ts # TypoKit configuration (optional)
├── package.json # Project dependencies & scripts
├── tsconfig.json # TypeScript configuration
├── src/
│ ├── app.ts # App factory — registers routes & middleware
│ ├── types.ts # Your schema type definitions
│ ├── routes/
│ │ └── todos/
│ │ ├── contracts.ts # RouteContract definitions
│ │ ├── handlers.ts # Handler implementations
│ │ └── middleware.ts # Route-specific middleware
│ ├── middleware/
│ │ └── require-auth.ts # Global middleware
│ └── services/
│ └── todo-service.ts # Business logic layer
└── .typokit/ # Auto-generated (git-ignored)

Your domain types live here. This is the single source of truth for your entire stack — validators, database schemas, OpenAPI specs, and client types are all generated from these interfaces.

/** @table */
export interface User {
/** @id @generated uuid */
id: string;
/** @format email */
email: string;
/** @minLength 2 */
name: string;
/** @generated now */
createdAt: Date;
}
export type CreateUserInput = Omit<User, "id" | "createdAt">;
export type PublicUser = Omit<User, "passwordHash">;

Types use JSDoc annotations (@table, @id, @format, @minLength, etc.) to express constraints. TypoKit reads these annotations at build time — no decorators or runtime schema libraries required.

Learn more in Schema-First Types.

This is where you register routes, middleware, and configure your server adapter. Routes are wired up explicitly here — not discovered from the file system.

import { createApp } from "@typokit/core";
import { nativeServer } from "@typokit/server-native";
import todoHandlers from "./routes/todos/handlers.ts";
export const app = createApp({
server: nativeServer(),
middleware: [],
routes: [
{ prefix: "/todos", handlers: todoHandlers },
],
});
app.listen({ port: 3000 }).then(() => {
console.log("Server running on http://localhost:3000");
});

src/routes/ — Route Contracts & Handlers

Section titled “src/routes/ — Route Contracts & Handlers”

Each route module lives in its own directory with a contract and handler file:

  • contracts.ts — Defines the route contract: HTTP method, path, params, query, body, and response types.
  • handlers.ts — Implements the contract. Handlers receive typed { params, query, body, ctx } input and return typed responses.
  • middleware.ts — Optional route-specific middleware.
src/routes/todos/contracts.ts
import type { RouteContract } from "@typokit/types";
import type { Todo, CreateTodoInput } from "../../types.ts";
export interface TodoRoutes {
"GET /todos": RouteContract<void, void, void, Todo[]>;
"POST /todos": RouteContract<void, void, CreateTodoInput, Todo>;
"GET /todos/:id": RouteContract<{ id: string }, void, void, Todo>;
}

Learn more in Routing and Handlers.

Typed middleware that narrows the context type, so downstream handlers know exactly what’s available.

Learn more in Middleware and Context.

An optional layer to keep handlers thin. Extract complex business logic, database calls, and external service interactions here.

The .typokit/ directory is produced by TypoKit’s Rust-powered build pipeline during typokit build or typokit dev. It is git-ignored and regenerated on every build.

.typokit/
├── validators/
│ ├── User.validator.ts # Input validation functions
│ ├── CreateUserInput.validator.ts
│ └── UpdateUserInput.validator.ts
├── routes/
│ ├── route-table.ts # Compiled route registry
│ └── compiled-router.ts # Radix-tree router (O(k) lookup)
├── schemas/
│ └── openapi.json # OpenAPI 3.1 spec
├── tests/
│ ├── users.contract.test.ts # Contract tests
│ └── todos.contract.test.ts
└── client/
└── index.ts # Type-safe API client source
DirectoryContentsPurpose
validators/One .validator.ts file per typeRuntime validation functions generated from your JSDoc constraints via Typia. Used by the server to validate request bodies and parameters.
routes/route-table.ts, compiled-router.tsA compiled route registry and radix-tree router for O(k) path matching. Maps HTTP methods + paths to handlers with pre-wired validation.
schemas/openapi.jsonA complete OpenAPI 3.1 specification generated from your types and route contracts. Served at /openapi.json in dev mode.
tests/*.contract.test.ts filesAuto-generated tests that verify handlers match their contracts — correct status codes, response shapes, and error types.
client/index.tsA type-safe fetch wrapper with methods for every route contract.

The configuration file lives at the project root and controls TypoKit’s build behavior:

typokit.config.ts
import { defineConfig } from "@typokit/cli";
export default defineConfig({
// Glob patterns for files containing type definitions
typeFiles: ["src/**/*.types.ts", "src/**/types.ts"],
// Glob patterns for files containing route contracts
routeFiles: [
"src/**/*.routes.ts",
"src/**/routes.ts",
"src/**/contracts.ts",
],
// Where to write generated artifacts
outputDir: ".typokit",
// Build output directory
distDir: "dist",
// TypeScript compiler to use
compiler: "tsc", // "tsc" | "tsup" | "swc"
// Additional compiler arguments
compilerArgs: [],
});

All options have sensible defaults. For most projects, a minimal config (or no config file at all) is sufficient:

// Minimal typokit.config.ts — all defaults
import { defineConfig } from "@typokit/cli";
export default defineConfig({});

Config resolution order:

  1. typokit.config.ts (or typokit.config.js)
  2. "typokit" field in package.json
  3. Built-in defaults

Understanding which files you own and which are generated is critical:

FileStatusNotes
src/types.ts✍️ User-maintainedYour types — the single source of truth
src/app.ts✍️ User-maintainedRoute registration & server setup
src/routes/**/contracts.ts✍️ User-maintainedRoute contracts
src/routes/**/handlers.ts✍️ User-maintainedYour handler logic
src/routes/**/middleware.ts✍️ User-maintainedRoute-specific middleware
src/middleware/**✍️ User-maintainedGlobal middleware
src/services/**✍️ User-maintainedYour business logic
typokit.config.ts✍️ User-maintainedBuild configuration
.typokit/**🤖 Auto-generatedBuild artifacts — git-ignored

When using the @typokit/plugin-axum plugin, the project structure differs significantly — it’s a Rust project rather than a TypeScript one:

my-axum-app/
├── typokit.config.ts # TypoKit configuration (with axumPlugin)
├── Cargo.toml # Rust project manifest (auto-generated)
├── types/ # TypeScript schema types (your source of truth)
│ ├── user.ts
│ └── todo.ts
├── routes/ # TypeScript route contracts
│ ├── users.ts
│ └── todos.ts
├── .typokit/ # Auto-generated Rust code (always overwritten)
│ ├── models/ # Rust structs with serde + validator + sqlx
│ ├── db/ # PgPool connection & CRUD functions
│ ├── router.rs # Axum Router
│ ├── app.rs # AppState
│ ├── error.rs # AppError enum
│ └── migrations/ # SQL migrations
└── src/ # Your Rust application code (never overwritten)
├── main.rs # Tokio entrypoint
├── lib.rs # Module bridge
├── handlers/ # Your handler implementations
├── services/ # Your business logic
└── middleware/ # Your middleware

The key difference: you still define your types in TypeScript (the single source of truth), but the build output is Rust code instead of TypeScript. The same overwrite model applies — .typokit/ is always regenerated, while src/handlers/, src/services/, and src/middleware/ are your code and never overwritten.

Now that you understand the project layout: