Getting Started
Installation
npm install itty-spec
# or
pnpm add itty-spec
# or
yarn add itty-specPeer Dependencies
itty-spec requires a Standard Schema V1 compatible library for validation. Install one of the following:
# For Zod (recommended)
npm install zod@v4
# For Valibot
npm install valibotEnvironment Setup
itty-spec works in any environment that supports the Fetch API. No special configuration is required, but you may need TypeScript configured:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"strict": true
}
}Quick Start
1) Define a contract
import { createContract } from "itty-spec";
import { z } from "zod";
const UserEntity = z.object({
id: z.uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18).optional(),
});
const CreateUserRequest = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18).optional(),
});
const ListUsersResponse = z.object({
users: z.array(UserEntity),
total: z.number(),
});
export const contract = createContract({
getUsers: {
path: "/users",
method: "GET",
headers: z.object({
"x-api-key": z.string(),
}),
query: z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10),
}),
responses: {
200: {
"application/json": { body: ListUsersResponse },
},
},
},
createUser: {
path: "/users",
method: "POST",
headers: z.object({
"x-api-key": z.string(),
}),
requests: {
"application/json": {
body: CreateUserRequest,
},
},
responses: {
200: {
"application/json": { body: UserEntity },
},
400: {
"application/json": { body: z.object({ error: z.string() }) },
},
},
},
});2) Implement the contract with a router
import { createRouter } from "itty-spec";
import { contract } from "./contract";
const router = createRouter({
contract,
handlers: {
getUsers: async (request) => {
const { page, limit } = request.validatedQuery;
return request.respond({
status: 200,
contentType: "application/json",
body: { users: [], total: 0 },
});
},
createUser: async (request) => {
const { name, email } = request.validatedBody;
return request.respond({
status: 200,
contentType: "application/json",
body: { id: "123", name, email },
});
},
},
});
export default {
fetch: router.fetch,
};Target Environments
itty-spec is designed to be lightweight and efficient, making it ideal for:
- Cloudflare Workers: Edge computing with minimal cold start times
- AWS Lambda: Serverless functions with size constraints
- Node.js servers: Traditional backend servers
- Bun: Fast JavaScript runtime
- Deno: Secure runtime for JavaScript and TypeScript
- Any Fetch-compatible environment: Works wherever the Fetch API is available
The library's minimal dependencies and small bundle size ensure fast startup times and low memory footprint, critical for edge and serverless deployments.
Your First API
Let's build a complete, working API step by step. This tutorial will show you how to create a simple todo API with full type safety.
Step 1: Define Your Schemas
First, create schemas for your data structures:
import { z } from "zod";
const TodoSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1),
completed: z.boolean(),
createdAt: z.string().datetime(),
});
const CreateTodoRequest = z.object({
title: z.string().min(1),
completed: z.boolean().default(false),
});Step 2: Create Your Contract
Define your API contract with all operations:
import { createContract } from "itty-spec";
export const contract = createContract({
getTodos: {
path: "/todos",
method: "GET",
query: z.object({
completed: z.boolean().optional(),
limit: z.number().min(1).max(100).default(10),
}),
responses: {
200: {
"application/json": {
body: z.object({
todos: z.array(TodoSchema),
total: z.number(),
}),
},
},
},
},
getTodo: {
path: "/todos/:id",
method: "GET",
responses: {
200: {
"application/json": { body: TodoSchema },
},
404: {
"application/json": { body: z.object({ error: z.string() }) },
},
},
},
createTodo: {
path: "/todos",
method: "POST",
requests: {
"application/json": { body: CreateTodoRequest },
},
responses: {
201: {
"application/json": { body: TodoSchema },
},
400: {
"application/json": { body: z.object({ error: z.string() }) },
},
},
},
});Step 3: Implement Handlers
Create handlers that receive typed, validated data:
import { createRouter } from "itty-spec";
import { contract } from "./contract";
// Simple in-memory store
const todos: Todo[] = [];
const router = createRouter({
contract,
handlers: {
getTodos: async (request) => {
const { completed, limit } = request.validatedQuery;
let filtered = todos;
if (completed !== undefined) {
filtered = todos.filter(t => t.completed === completed);
}
return request.respond({
status: 200,
contentType: "application/json",
body: {
todos: filtered.slice(0, limit),
total: filtered.length,
},
});
},
getTodo: async (request) => {
const { id } = request.validatedParams;
const todo = todos.find(t => t.id === id);
if (!todo) {
return request.respond({
status: 404,
contentType: "application/json",
body: { error: "Todo not found" },
});
}
return request.respond({
status: 200,
contentType: "application/json",
body: todo,
});
},
createTodo: async (request) => {
const { title, completed } = request.validatedBody;
const todo = {
id: crypto.randomUUID(),
title,
completed: completed ?? false,
createdAt: new Date().toISOString(),
};
todos.push(todo);
return request.respond({
status: 201,
contentType: "application/json",
body: todo,
});
},
},
});
export default { fetch: router.fetch };Step 4: Deploy
Now you can deploy this to any Fetch-compatible environment:
Cloudflare Workers:
// Already done! Just export the fetch handler
export default { fetch: router.fetch };Node.js:
import { createServer } from "http";
import { createServerAdapter } from "@whatwg-node/server";
const adapter = createServerAdapter(router.fetch);
const server = createServer(adapter);
server.listen(3000);Bun:
Bun.serve({ fetch: router.fetch });Common Pitfalls
1. Forgetting as const for Path Parameters
For automatic path parameter extraction, use as const:
// ✅ Good - path params are extracted
const contract = createContract({
getUser: {
path: "/users/:id", // TypeScript infers { id: string }
method: "GET",
// ...
},
} as const);
// ❌ Bad - path params may not be extracted
const contract = createContract({
getUser: {
path: "/users/:id", // May fall back to EmptyObject
method: "GET",
// ...
},
});2. Not Providing Required Handlers
Every operation in your contract should have a corresponding handler:
// ✅ Good
const router = createRouter({
contract,
handlers: {
getUsers: async (request) => { /* ... */ },
createUser: async (request) => { /* ... */ },
},
});
// ❌ Bad - missing handler will cause runtime errors
const router = createRouter({
contract,
handlers: {
getUsers: async (request) => { /* ... */ },
// createUser is missing!
},
});3. Mismatched Response Types
Ensure your response matches the contract exactly:
// ✅ Good
return request.respond({
status: 200,
contentType: "application/json",
body: { users: [], total: 0 }, // Matches contract
});
// ❌ Bad - TypeScript error!
return request.respond({
status: 200,
contentType: "application/json",
body: { users: [] }, // Missing 'total' field
});4. Incorrect Content-Type Handling
When using multiple content types, ensure you handle them correctly:
// ✅ Good - check content type from headers
const contentType = request.validatedHeaders.get("content-type");
if (contentType === "text/html") {
return request.respond({
status: 200,
contentType: "text/html",
body: "<html>...</html>",
});
}
// ❌ Bad - assuming content type
return request.respond({
status: 200,
contentType: "text/html", // May not match request
body: "<html>...</html>",
});Next Steps
- Learn about Core Concepts to understand how itty-spec works
- Explore Contracts to master contract definitions
- Check out Examples for real-world patterns