Router Configuration
The createRouter function is the heart of itty-spec. It binds your contract to handlers and configures the request/response pipeline.
Basic Usage
import { createRouter } from "itty-spec";
const router = createRouter({
contract,
handlers: {
// Your handlers here
},
});Complete Options Reference
createRouter<TContract, RequestType, Args>({
contract: TContract, // Required: Your contract definition
handlers: { // Required: Handlers for each operation
[operationId: string]: HandlerFunction;
},
base?: string, // Optional: Base path for all routes
missing?: HandlerFunction, // Optional: Handler for unmatched routes
before?: RequestHandler[], // Optional: Middleware to run before handlers
finally?: ResponseHandler[], // Optional: Middleware to run after handlers
format?: ResponseHandler, // Optional: Custom response formatter
})Contract
The contract option is required and defines your API operations:
const router = createRouter({
contract: myContract,
handlers: { /* ... */ },
});Handlers
Handlers implement the business logic for each operation in your contract. Each handler receives a typed request object:
const router = createRouter({
contract,
handlers: {
getUser: async (request) => {
// request is fully typed based on the contract
const { id } = request.validatedParams;
// ...
},
createUser: async (request) => {
const body = request.validatedBody;
// ...
},
},
});Handler Type Signature
type HandlerFunction = (
request: ContractRequest<Operation>,
...args: Args
) => Promise<ContractOperationResponse<Operation>>;Base Path
Use base to prefix all routes with a common path:
const router = createRouter({
contract,
handlers,
base: "/api/v1", // All routes are prefixed with /api/v1
});
// Contract path: "/users"
// Actual route: "/api/v1/users"This is useful for:
- API versioning
- Mounting routers at specific paths
- Organizing routes by domain
Missing Route Handler
The missing option defines what happens when no route matches:
const router = createRouter({
contract,
handlers,
missing: async (request) => {
return request.respond({
status: 404,
contentType: "application/json",
body: {
error: "Not Found",
path: new URL(request.url).pathname,
},
});
},
});Default: Returns a 404 response with a JSON error message.
Middleware
Middleware functions run at different stages of the request lifecycle.
Before Middleware
before middleware runs after validation but before your handler:
const router = createRouter({
contract,
handlers,
before: [
// Logging middleware
async (request) => {
console.log(`${request.method} ${request.url}`);
},
// Authentication middleware
async (request) => {
const auth = request.headers.get("authorization");
if (!auth) {
throw new Error("Unauthorized");
}
},
],
});Built-in before middleware (runs automatically):
withParams- Extracts path parameterswithMatchingContractOperation- Finds matching operationwithSpecValidation- Validates request datawithResponseHelpers- Addsrespond()method
Finally Middleware
finally middleware runs after your handler, before the response is sent:
const router = createRouter({
contract,
handlers,
finally: [
// Response timing middleware
async (request, response) => {
const duration = Date.now() - request.startTime;
response.headers.set("x-response-time", `${duration}ms`);
return response;
},
// CORS middleware
async (request, response) => {
response.headers.set("access-control-allow-origin", "*");
return response;
},
],
});Built-in finally middleware:
withMissingHandler- Handles 404swithContractFormat- Formats responses
Custom Response Formatting
The format option allows you to customize how responses are formatted:
const router = createRouter({
contract,
handlers,
format: async (request, response) => {
// Custom formatting logic
if (response.body && typeof response.body === 'object') {
return new Response(
JSON.stringify(response.body, null, 2), // Pretty print
{
status: response.status,
headers: response.headers,
}
);
}
return response;
},
});Default: Automatically formats responses based on content type and contract.
Additional Handler Arguments
You can pass additional arguments to handlers using TypeScript generics:
type Context = {
db: Database;
logger: Logger;
};
const router = createRouter<typeof contract, IRequest, [Context]>({
contract,
handlers: {
getUser: async (request, context) => {
// context is typed as Context
const user = await context.db.findUser(request.validatedParams.id);
context.logger.info("User retrieved", { userId: user.id });
// ...
},
},
});
// When calling router.fetch, pass context:
router.fetch(request, { db, logger });This is useful for:
- Dependency injection
- Request context
- Shared services
Advanced Patterns
Multiple Routers
Combine multiple routers for different domains:
const userRouter = createRouter({
contract: userContract,
handlers: userHandlers,
base: "/users",
});
const productRouter = createRouter({
contract: productContract,
handlers: productHandlers,
base: "/products",
});
// Combine in main router
const mainRouter = Router();
mainRouter.all("/users/*", userRouter.fetch);
mainRouter.all("/products/*", productRouter.fetch);Conditional Middleware
Apply middleware conditionally:
const router = createRouter({
contract,
handlers,
before: [
...(process.env.NODE_ENV === 'development' ? [loggingMiddleware] : []),
authMiddleware,
],
});Error Handling
Customize error handling:
const router = createRouter({
contract,
handlers,
// Error handling is built-in, but you can customize
// by catching errors in middleware
before: [
async (request) => {
try {
// Your logic
} catch (error) {
// Custom error handling
throw error; // Re-throw to use default handler
}
},
],
});Type Parameters
createRouter accepts three type parameters:
createRouter<
TContract extends ContractDefinition, // Your contract type
RequestType extends IRequest = IRequest, // Request type (default: IRequest)
Args extends any[] = any[] // Additional handler arguments
>(options)Custom Request Type
Extend the request type for additional properties:
interface AuthenticatedRequest extends IRequest {
userId: string;
userRole: string;
}
const router = createRouter<typeof contract, AuthenticatedRequest>({
contract,
handlers: {
getUser: async (request) => {
// request.userId and request.userRole are available
const userId = request.userId;
// ...
},
},
});Best Practices
1. Organize Handlers by Domain
// handlers/users.ts
export const userHandlers = {
getUser: async (request) => { /* ... */ },
createUser: async (request) => { /* ... */ },
};
// handlers/products.ts
export const productHandlers = {
getProduct: async (request) => { /* ... */ },
};
// index.ts
const router = createRouter({
contract,
handlers: {
...userHandlers,
...productHandlers,
},
});2. Use Base Paths for Versioning
const v1Router = createRouter({
contract: v1Contract,
handlers: v1Handlers,
base: "/api/v1",
});
const v2Router = createRouter({
contract: v2Contract,
handlers: v2Handlers,
base: "/api/v2",
});3. Keep Middleware Focused
// ✅ Good - single responsibility
const authMiddleware = async (request) => {
// Only authentication logic
};
const loggingMiddleware = async (request) => {
// Only logging logic
};
// ❌ Bad - mixed concerns
const authAndLoggingMiddleware = async (request) => {
// Authentication AND logging
};4. Handle Missing Routes Gracefully
missing: async (request) => {
return request.respond({
status: 404,
contentType: "application/json",
body: {
error: "Not Found",
message: `Route ${new URL(request.url).pathname} not found`,
availableRoutes: ["/users", "/products"],
},
});
},Related Topics
- Contracts - Learn about contract definitions
- Middleware - Deep dive into middleware
- Error Handling - Handle errors effectively
- Validation - Understand validation flow