Advanced Patterns
This guide covers advanced patterns and techniques for building complex APIs with itty-spec.
Multi-Domain Contracts
Organize large APIs by splitting contracts into domains:
ts
// contracts/users.contract.ts
export const usersContract = createContract({
getUser: { /* ... */ },
createUser: { /* ... */ },
});
// contracts/products.contract.ts
export const productsContract = createContract({
getProduct: { /* ... */ },
createProduct: { /* ... */ },
});
// contracts/orders.contract.ts
export const ordersContract = createContract({
getOrder: { /* ... */ },
createOrder: { /* ... */ },
});
// contracts/index.ts
export const contract = {
...usersContract,
...productsContract,
...ordersContract,
};Contract Composition
Compose contracts from smaller pieces:
ts
// contracts/base.contract.ts
export const baseContract = createContract({
healthCheck: {
path: "/health",
method: "GET",
responses: {
200: {
"application/json": {
body: z.object({ status: z.literal("ok") }),
},
},
},
},
});
// contracts/api.contract.ts
export const apiContract = {
...baseContract,
...usersContract,
...productsContract,
};Conditional Responses
Handle different response types based on conditions:
ts
const handler = async (request) => {
const user = await getUser(request.validatedParams.id);
if (!user) {
return request.respond({
status: 404,
contentType: "application/json",
body: { error: "User not found" },
});
}
if (user.deleted) {
return request.respond({
status: 410,
contentType: "application/json",
body: { error: "User has been deleted" },
});
}
return request.respond({
status: 200,
contentType: "application/json",
body: user,
});
};Dynamic Content Types
Select content type based on request headers:
ts
const handler = async (request) => {
const accept = request.headers.get("accept") || "application/json";
const user = await getUser(request.validatedParams.id);
if (accept.includes("text/html")) {
return request.respond({
status: 200,
contentType: "text/html",
body: renderUserPage(user),
});
}
if (accept.includes("application/xml")) {
return request.respond({
status: 200,
contentType: "application/xml",
body: renderUserXML(user),
});
}
// Default to JSON
return request.respond({
status: 200,
contentType: "application/json",
body: user,
});
};File Uploads
Handle file uploads with multipart form data:
ts
const contract = createContract({
uploadFile: {
path: "/files",
method: "POST",
requests: {
"multipart/form-data": {
body: z.object({
file: z.instanceof(File),
description: z.string().optional(),
}),
},
},
responses: {
201: {
"application/json": {
body: z.object({
id: z.string(),
filename: z.string(),
size: z.number(),
}),
},
},
},
},
});
const handler = async (request) => {
const formData = await request.formData();
const file = formData.get("file") as File;
const description = formData.get("description") as string;
// Process file
const fileId = await saveFile(file);
return request.respond({
status: 201,
contentType: "application/json",
body: {
id: fileId,
filename: file.name,
size: file.size,
},
});
};Streaming Responses
Stream large responses:
ts
const handler = async (request) => {
const stream = new ReadableStream({
async start(controller) {
const data = await fetchLargeDataset();
for (const item of data) {
controller.enqueue(JSON.stringify(item) + "\n");
}
controller.close();
},
});
return new Response(stream, {
status: 200,
headers: {
"content-type": "application/x-ndjson",
},
});
};WebSocket Integration
While itty-spec is designed for HTTP, you can integrate WebSockets:
ts
// Handle WebSocket upgrade
const router = createRouter({
contract: {
upgradeWebSocket: {
path: "/ws",
method: "GET",
headers: z.object({
upgrade: z.literal("websocket"),
}),
responses: {
101: {
"application/json": { body: z.any() },
},
},
},
},
handlers: {
upgradeWebSocket: async (request) => {
// Handle WebSocket upgrade
// This is environment-specific
return new Response(null, { status: 101 });
},
},
});Request Context
Pass additional context to handlers:
ts
type Context = {
db: Database;
logger: Logger;
cache: Cache;
};
const router = createRouter<typeof contract, IRequest, [Context]>({
contract,
handlers: {
getUser: async (request, context) => {
context.logger.info("Fetching user", { id: request.validatedParams.id });
const cached = await context.cache.get(request.validatedParams.id);
if (cached) {
return request.respond({
status: 200,
contentType: "application/json",
body: cached,
});
}
const user = await context.db.findUser(request.validatedParams.id);
await context.cache.set(request.validatedParams.id, user);
return request.respond({
status: 200,
contentType: "application/json",
body: user,
});
},
},
});
// Pass context when calling
router.fetch(request, { db, logger, cache });Middleware Chains
Create reusable middleware chains:
ts
const authMiddleware = async (request: IRequest) => {
const auth = request.headers.get("authorization");
if (!auth) {
throw new Error("Unauthorized");
}
(request as any).user = await getUserFromToken(auth);
};
const loggingMiddleware = async (request: IRequest) => {
console.log(`${request.method} ${request.url}`);
};
const errorHandlingMiddleware = async (request: IRequest) => {
try {
// Your logic
} catch (error) {
// Handle error
throw error;
}
};
// Reuse across routers
const commonMiddleware = [
loggingMiddleware,
authMiddleware,
errorHandlingMiddleware,
];
const router1 = createRouter({
contract: contract1,
handlers: handlers1,
before: commonMiddleware,
});
const router2 = createRouter({
contract: contract2,
handlers: handlers2,
before: commonMiddleware,
});Versioning
Version your API using base paths:
ts
const v1Router = createRouter({
contract: v1Contract,
handlers: v1Handlers,
base: "/api/v1",
});
const v2Router = createRouter({
contract: v2Contract,
handlers: v2Handlers,
base: "/api/v2",
});
// Combine in main router
const mainRouter = Router();
mainRouter.all("/api/v1/*", v1Router.fetch);
mainRouter.all("/api/v2/*", v2Router.fetch);Database Transactions
Handle database transactions:
ts
const handler = async (request) => {
const transaction = await db.beginTransaction();
try {
const user = await transaction.createUser(request.validatedBody);
await transaction.createUserProfile(user.id, request.validatedBody.profile);
await transaction.commit();
return request.respond({
status: 201,
contentType: "application/json",
body: user,
});
} catch (error) {
await transaction.rollback();
throw error;
}
};Caching Strategies
Implement caching:
ts
const cache = new Map<string, { data: unknown; expires: number }>();
const handler = async (request) => {
const cacheKey = `${request.method}:${request.url}`;
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return request.respond({
status: 200,
contentType: "application/json",
body: cached.data,
});
}
const data = await fetchData();
cache.set(cacheKey, {
data,
expires: Date.now() + 60000, // 1 minute
});
return request.respond({
status: 200,
contentType: "application/json",
body: data,
});
};Rate Limiting
Implement rate limiting:
ts
const rateLimiter = new Map<string, { count: number; resetAt: number }>();
const withRateLimit = (maxRequests: number = 100, windowMs: number = 60000) => {
return async (request: IRequest) => {
const key = request.headers.get("x-forwarded-for") || "unknown";
const now = Date.now();
const limit = rateLimiter.get(key);
if (limit && limit.resetAt > now) {
if (limit.count >= maxRequests) {
throw new Error("Rate limit exceeded");
}
limit.count++;
} else {
rateLimiter.set(key, { count: 1, resetAt: now + windowMs });
}
};
};
const router = createRouter({
contract,
handlers,
before: [withRateLimit(100, 60000)],
});Related Topics
- Best Practices - General best practices
- Middleware - Middleware patterns
- Router Configuration - Router setup