Context
The route handler context provides type-safe access to request data, validated against your input schemas defined with .input().
Handler Context
When you define a route with .handler(), you receive a context object with access to validated request data and response helpers.
typescriptimport { route } from '@spfn/core/route'; import { Type } from '@sinclair/typebox'; export const getUser = route.get('/users/:id') .input({ params: Type.Object({ id: Type.String() }) }) .handler(async (c) => { // c provides type-safe access to request data const { params } = await c.data(); // ^? { id: string } return { id: params.id, name: 'John Doe' }; });
Context Methods
c.data()
Type-safe access to all validated request data (params, query, body, headers).
typescriptexport const updateUser = route.put('/users/:id') .input({ params: Type.Object({ id: Type.String() }), body: Type.Object({ name: Type.String(), email: Type.String() }) }) .handler(async (c) => { const { params, body } = await c.data(); // params: { id: string } // body: { name: string; email: string } const user = await updateUserById(params.id, body); return user; });
Data Properties
| Property | Description |
|---|---|
params | URL path parameters (e.g., /users/:id) |
query | Query string parameters (e.g., ?page=1) |
body | Request body (POST, PUT, PATCH) |
headers | Request headers (when defined in input) |
c.json()
Return JSON response with optional status code and headers.
typescript// Basic usage return c.json({ id: 1, name: 'John' }); // With status code return c.json({ id: 1, name: 'John' }, 201); // With custom headers return c.json( { id: 1, name: 'John' }, 200, { 'X-Custom-Header': 'value' } );
c.created()
Return 201 Created response with optional Location header.
typescriptexport const createUser = route.post('/users') .input({ body: Type.Object({ name: Type.String(), email: Type.String() }) }) .handler(async (c) => { const { body } = await c.data(); const user = await createUserInDb(body); // Returns 201 with Location header return c.created(user, `/users/${user.id}`); });
c.noContent()
Return 204 No Content response (typically for DELETE operations).
typescriptexport const deleteUser = route.delete('/users/:id') .input({ params: Type.Object({ id: Type.String() }) }) .handler(async (c) => { const { params } = await c.data(); await deleteUserById(params.id); return c.noContent(); });
c.success()
Return standardized success response format.
typescriptreturn c.success({ id: 1, name: 'John' }); // Response: { success: true, data: { id: 1, name: 'John' } } // With metadata return c.success( { id: 1, name: 'John' }, { timestamp: Date.now() } ); // Response: { success: true, data: {...}, meta: { timestamp: ... } }
c.paginated()
Return paginated list with pagination metadata.
typescriptexport const getUsers = route.get('/users') .input({ query: Type.Object({ page: Type.Optional(Type.Number({ default: 1 })), limit: Type.Optional(Type.Number({ default: 10 })) }) }) .handler(async (c) => { const { query } = await c.data(); const { page = 1, limit = 10 } = query; const users = await findUsers({ page, limit }); const total = await countUsers(); return c.paginated(users, page, limit, total); }); // Response format: // { // success: true, // data: [...], // meta: { // pagination: { // page: 1, // limit: 10, // total: 100, // totalPages: 10 // } // } // }
c.accepted()
Return 202 Accepted for async operations.
typescriptexport const processJob = route.post('/jobs') .input({ body: Type.Object({ type: Type.String() }) }) .handler(async (c) => { const { body } = await c.data(); const job = await queueJob(body); return c.accepted({ jobId: job.id, status: 'queued' }); });
c.notModified()
Return 304 Not Modified for cache validation.
typescriptexport const getResource = route.get('/resources/:id') .input({ params: Type.Object({ id: Type.String() }) }) .handler(async (c) => { const etag = c.raw.req.header('If-None-Match'); const resource = await findResource(); const currentEtag = generateEtag(resource); if (etag === currentEtag) { return c.notModified(); } c.raw.header('ETag', currentEtag); return c.success(resource); });
c.raw - Hono Context
Access the underlying Hono context for advanced use cases.
typescriptexport const getUser = route.get('/users/:id') .input({ params: Type.Object({ id: Type.String() }) }) .handler(async (c) => { // Access raw Hono context const honoContext = c.raw; // Get request headers const auth = honoContext.req.header('Authorization'); // Set response headers honoContext.header('X-Request-ID', 'req-123'); // Access middleware-set values const user = honoContext.get('user'); // Get client IP const ip = honoContext.req.header('x-forwarded-for'); const { params } = await c.data(); return { id: params.id }; });
Common Patterns
GET with Query Parameters
typescriptexport const searchUsers = route.get('/users/search') .input({ query: Type.Object({ q: Type.String(), limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })), offset: Type.Optional(Type.Number({ minimum: 0 })) }) }) .handler(async (c) => { const { query } = await c.data(); const { q, limit = 10, offset = 0 } = query; const results = await searchUsers(q, { limit, offset }); return { items: results, query: q }; });
POST with Body
typescriptexport const createTeam = route.post('/teams') .input({ body: Type.Object({ name: Type.String({ minLength: 1 }), slug: Type.String({ pattern: '^[a-z0-9-]+$' }), description: Type.Optional(Type.String()) }) }) .handler(async (c) => { const { body } = await c.data(); const team = await createTeamInDb(body); return c.created(team, `/teams/${team.id}`); });
PUT with Params and Body
typescriptexport const updateTeam = route.put('/teams/:id') .input({ params: Type.Object({ id: Type.String() }), body: Type.Object({ name: Type.Optional(Type.String()), description: Type.Optional(Type.String()) }) }) .handler(async (c) => { const { params, body } = await c.data(); const team = await updateTeamById(params.id, body); return team; });
Accessing Middleware Data
Middleware can store data in Hono context for handlers to access:
typescript// Middleware sets user export const authMiddleware = defineMiddleware('auth', async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', ''); const user = await verifyToken(token); c.set('user', user); await next(); }); // Handler accesses user via c.raw.get() export const getProfile = route.get('/profile') .handler(async (c) => { const user = c.raw.get('user'); return { id: user.id, email: user.email }; });
Custom Response Headers
typescriptexport const getUsers = route.get('/users') .input({ query: Type.Object({ limit: Type.Optional(Type.Number()), offset: Type.Optional(Type.Number()) }) }) .handler(async (c) => { const { query } = await c.data(); const { limit = 10, offset = 0 } = query; const users = await findUsers({ limit, offset }); const total = await countUsers(); // Add pagination headers return c.json( { items: users, total }, 200, { 'X-Total-Count': String(total), 'X-Page-Size': String(limit), 'X-Page-Offset': String(offset) } ); });
Error Responses
typescriptimport { NotFoundError, ValidationError } from '@spfn/core/errors'; export const getUser = route.get('/users/:id') .input({ params: Type.Object({ id: Type.String() }) }) .handler(async (c) => { const { params } = await c.data(); const user = await findUserById(params.id); if (!user) { throw new NotFoundError({ resource: 'User' }); } return user; });
Type Inference
Types are automatically inferred from your .input() definition:
typescriptexport const createPost = route.post('/posts') .input({ body: Type.Object({ title: Type.String(), content: Type.String(), published: Type.Boolean() }) }) .handler(async (c) => { const { body } = await c.data(); // body is typed as: { title: string; content: string; published: boolean } return { id: '1', ...body }; // Return type is inferred for client-side type safety });
Extracting Types
typescriptimport type { InferRouteTypes } from '@spfn/core/route'; // Extract input and output types from a route type CreatePostTypes = InferRouteTypes<typeof createPost>; type CreatePostInput = CreatePostTypes['input']; // { body: { title: string; ... } } type CreatePostOutput = CreatePostTypes['output']; // { id: string; title: string; ... }
Best Practices
1. Always Use c.data()
typescript// ✅ Good: Use c.data() for validated data const { params, body } = await c.data(); // ❌ Bad: Access raw request directly (bypasses validation) const body = await c.raw.req.json();
2. Use Response Helpers
typescript// ✅ Good: Use appropriate response helpers return c.created(user, `/users/${user.id}`); // 201 Created return c.noContent(); // 204 No Content return c.paginated(items, page, limit, total); // Paginated response // ❌ Bad: Manual response construction return new Response(JSON.stringify(user), { status: 201, headers: { 'Content-Type': 'application/json' } });
3. Use c.raw Only When Necessary
typescript// ✅ Good: Use c.raw for Hono-specific features const requestId = c.raw.get('requestId'); const ip = c.raw.req.header('x-forwarded-for'); // ✅ Good: Use c.data() for request data const { params, query, body } = await c.data();
4. Handle Errors with SPFN Error Classes
typescriptimport { NotFoundError, ForbiddenError } from '@spfn/core/errors'; // ✅ Good: Throw typed errors if (!user) { throw new NotFoundError({ resource: 'User' }); } if (!hasPermission) { throw new ForbiddenError({ message: 'Access denied' }); } // ❌ Bad: Generic errors throw new Error('User not found');
Note: Type Safety Benefits
- Compile-time checks: TypeScript catches type errors before runtime
- IntelliSense: Full autocomplete for params, query, and body
- Refactoring safety: Changes to input schemas propagate automatically
- Runtime validation: TypeBox validates all input data
Next: Middleware
Learn about built-in middleware and how to create custom middleware.