Define Handlers
Ecopages provides two helper functions for defining type-safe API handlers: defineApiHandler for individual routes and defineGroupHandler for related routes that share a prefix and middleware.
These helpers enable you to define handlers outside your main app file while preserving full TypeScript inference for path parameters, middleware context, and request schemas.
Why Use Define Helpers
When defining handlers inline, TypeScript can infer types from context:
app.get('/api/posts/:id', async ({ request }) => {
const { id } = request.params; // TypeScript knows id exists
return Response.json({ id });
});However, when you extract handlers to separate files, you lose that inference:
// handlers/blog.ts - Type inference is lost
export const getPost = async ({ request }) => {
const { id } = request.params; // Error: params is { [x: string]: string }
};The defineApiHandler and defineGroupHandler helpers solve this by capturing the path literal type and middleware context at definition time.
defineApiHandler
Use defineApiHandler to create a self-contained handler with its path, method, and optional middleware or schema.
Basic Usage
// handlers/blog.ts
import { defineApiHandler } from '@ecopages/core/adapters/bun';
export const list = defineApiHandler({
path: '/api/posts',
method: 'GET',
handler: async ({ response }) => {
const posts = await getPosts();
return response.json(posts);
},
});
export const detail = defineApiHandler({
path: '/api/posts/:id',
method: 'GET',
handler: async ({ request, response }) => {
// request.params.id is correctly typed as string
const post = await getPost(request.params.id);
return response.json(post);
},
});Registration
Register handlers directly with HTTP method functions:
// app.ts
import { EcopagesApp } from '@ecopages/core/adapters/bun/create-app';
import * as blog from './handlers/blog';
const app = new EcopagesApp({ appConfig });
app.get(blog.list);
app.get(blog.detail);
await app.start();With Request Schema
Define a schema to get typed access to request body, query parameters, and headers:
import { defineApiHandler } from '@ecopages/core/adapters/bun';
import { z } from 'zod';
const createPostSchema = {
body: z.object({
title: z.string().min(1),
content: z.string(),
}),
query: z.object({
draft: z.string().optional(),
}),
};
export const createPost = defineApiHandler({
path: '/api/posts',
method: 'POST',
schema: createPostSchema,
handler: async ({ body, query, response }) => {
// body and query are automatically parsed and typed from the schema
// body: { title: string; content: string }
// query: { draft?: string }
const post = await createPost(body, query.draft === 'true');
return response.status(201).json(post);
},
});With Middleware
Attach route-specific middleware that extends the handler context:
import { defineApiHandler } from '@ecopages/core/adapters/bun';
import { rateLimitMiddleware } from './middleware/rate-limit';
export const sensitiveEndpoint = defineApiHandler({
path: '/api/sensitive',
method: 'POST',
middleware: [rateLimitMiddleware],
handler: async ({ response, rateLimitRemaining }) => {
// rateLimitRemaining is typed from the middleware
return response.json({ remaining: rateLimitRemaining });
},
});defineGroupHandler
Use defineGroupHandler when you have multiple routes that share a URL prefix and middleware. This is ideal for authenticated sections or resource-based APIs.
Basic Usage
// handlers/admin.ts
import { defineGroupHandler } from '@ecopages/core/adapters/bun';
import { authMiddleware } from './middleware/auth';
import type { AuthenticatedContext } from './middleware/auth';
export const adminGroup = defineGroupHandler({
prefix: '/admin',
middleware: [authMiddleware],
routes: (define) => [
define({
path: '/',
method: 'GET',
handler: async (ctx) => {
// ctx.session is typed from authMiddleware
return ctx.response.json({ user: ctx.session.user });
},
}),
define({
path: '/posts/:id',
method: 'GET',
handler: async (ctx) => {
// ctx.session is typed from authMiddleware
// ctx.request.params.id is typed from the path
const post = await getPost(ctx.request.params.id);
return ctx.response.json(post);
},
}),
define({
path: '/posts/:id',
method: 'DELETE',
handler: async (ctx) => {
await deletePost(ctx.request.params.id);
return ctx.response.status(204).text('');
},
}),
],
});Registration
Register the entire group with app.group():
// app.ts
import { EcopagesApp } from '@ecopages/core/adapters/bun/create-app';
import { adminGroup } from './handlers/admin';
import appConfig from './eco.config';
const app = new EcopagesApp({ appConfig });
app.group(adminGroup);
await app.start();The group registration:
- Prepends the prefix to each route path (
/becomes/admin,/posts/:idbecomes/admin/posts/:id) - Applies group middleware before route-specific middleware
- Preserves all type inference
With Route-Level Schema
Individual routes within a group can define their own schema:
import { z } from 'zod';
const createPostSchema = {
body: z.object({
title: z.string().min(1),
content: z.string(),
}),
};
export const adminGroup = defineGroupHandler({
prefix: '/admin',
middleware: [authMiddleware],
routes: (define) => [
define({
path: '/posts',
method: 'POST',
schema: createPostSchema,
handler: async (ctx) => {
// ctx.session from middleware + ctx.body from schema
const post = await createPost(ctx.body, ctx.session.user.id);
return ctx.response.status(201).json(post);
},
}),
],
});Type Inference Details
Path Parameters
Path parameters are inferred from the literal path string:
defineApiHandler({
path: '/api/users/:userId/posts/:postId',
method: 'GET',
handler: async ({ request }) => {
// request.params is typed as { userId: string; postId: string }
const { userId, postId } = request.params;
},
});Middleware Context
Middleware can extend the handler context with additional properties:
// middleware/auth.ts
import type { BunMiddleware } from '@ecopages/core/adapters/bun';
export type AuthenticatedContext = {
session: { user: User; token: string };
};
export const authMiddleware: BunMiddleware<AuthenticatedContext> = async (ctx, next) => {
const token = ctx.request.headers.get('Authorization');
const user = await validateToken(token);
if (!user) {
return ctx.response.status(401).json({ error: 'Unauthorized' });
}
ctx.session = { user, token };
return next();
};When used with defineGroupHandler, all routes receive the extended context automatically.
Complete Example
// handlers/blog.ts
import { defineApiHandler, defineGroupHandler } from '@ecopages/core/adapters/bun';
import { authMiddleware, type AuthenticatedContext } from '../middleware/auth';
import { z } from 'zod';
const createPostSchema = {
body: z.object({
title: z.string().min(1),
content: z.string(),
}),
};
const updatePostSchema = {
body: z.object({
title: z.string().min(1).optional(),
content: z.string().optional(),
}),
};
// Public routes
export const list = defineApiHandler({
path: '/api/posts',
method: 'GET',
handler: async ({ response }) => {
const posts = await getPublicPosts();
return response.json(posts);
},
});
export const detail = defineApiHandler({
path: '/api/posts/:id',
method: 'GET',
handler: async ({ request, response }) => {
const post = await getPost(request.params.id);
if (!post) return response.status(404).json({ error: 'Not found' });
return response.json(post);
},
});
// Protected routes
export const protectedGroup = defineGroupHandler({
prefix: '/api/posts',
middleware: [authMiddleware],
routes: (define) => [
define({
path: '/',
method: 'POST',
schema: createPostSchema,
handler: async (ctx) => {
const post = await createPost(ctx.body, ctx.session.user.id);
return ctx.response.status(201).json(post);
},
}),
define({
path: '/:id',
method: 'PUT',
schema: updatePostSchema,
handler: async (ctx) => {
const post = await updatePost(ctx.request.params.id, ctx.body, ctx.session.user.id);
return ctx.response.json(post);
},
}),
define({
path: '/:id',
method: 'DELETE',
handler: async (ctx) => {
await deletePost(ctx.request.params.id, ctx.session.user.id);
return ctx.response.status(204).text('');
},
}),
],
});// app.ts
import { EcopagesApp } from '@ecopages/core/adapters/bun/create-app';
import * as blog from './handlers/blog';
const app = new EcopagesApp({ appConfig });
// Public
app.get(blog.list);
app.get(blog.detail);
// Protected
app.group(blog.protectedGroup);
await app.start();Handler Object Shape
For reference, here is the shape of objects produced by the define helpers:
interface ApiHandler<TPath, TRequest, TServer> {
path: TPath;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
handler: (context: ApiHandlerContext<TRequest, TServer>) => Promise<Response> | Response;
middleware?: Middleware[];
schema?: RouteSchema;
}
interface GroupHandler<TPrefix> {
prefix: TPrefix;
middleware?: Middleware[];
routes: ApiHandler[];
}Both can be passed directly to app.[method]() or app.group() respectively.