API Handlers
Ecopages provides a straightforward way to define API endpoints within your application. This allows you to build server-side logic, fetch data, or perform actions directly from your Ecopages project.
Defining Handlers
You define API handlers using methods on your app instance, typically in your app.ts file. Each method corresponds to an HTTP verb (GET, POST, etc.).
// filepath: app.ts
import { createApp } from '@ecopages/core/create-app';
import appConfig from './eco.config';
const app = await createApp({ appConfig });
// Define a GET handler
app.get('/api/greet', async ({ response }) => {
return response.text('Hello from the API!');
});
// Define a POST handler
app.post('/api/submit', async ({ request, response }) => {
const body = await request.json(); // Assuming JSON body
console.log('Received data:', body);
return response.json({ success: true, received: body });
});
await app.start();Handler Context
Every API handler function receives a HandlerContext object as its argument. This object provides access to essential information and utilities for handling the request and constructing the response.
app.get('/api/example/:id', async ({ request, response, params }) => {
const { id } = params;
const userAgent = request.headers.get('user-agent');
const queryParam = new URL(request.url).searchParams.get('search');
const body = {
userId: id,
agent: userAgent,
query: queryParam,
};
return response.status(200).json(body);
});Validating Route Params with schema
You can validate and type route parameters through schema.params.
import { z } from 'zod';
app.get(
'/api/users/:id',
async ({ params, response }) => {
return response.json({ userId: params.id });
},
{
schema: {
params: z.object({
id: z.string().uuid(),
}),
},
},
);The HandlerContext contains:
request: The incoming request object (specific to the adapter, e.g.,Requestfor Bun and Node).- Standard
Requestproperties likeheaders,method,url, etc. - Methods like
json(),text(),formData()to parse the request body.
- Standard
params: Route parameters parsed from the path (e.g.,:idin/api/users/:id).response: An instance ofApiResponseBuilder, a utility for constructingResponseobjects fluently.server: The underlying server instance (e.g., Bun's server object), which exposes advanced utilities such asrequestIP,reload, and other debugging or runtime methods.body: The parsed and optionally validated request body. Automatically typed when using schemas.query: Parsed and optionally validated query parameters. Automatically typed when using schemas.headers: Parsed and optionally validated request headers. Automatically typed when using schemas.services: An object containing framework services likecache.
Accessing the Server Instance
You can use the server property in the handler context for advanced use cases. For example, to get the request IP address:
app.get('/api/hello', async ({ response, request, server }) => {
return response.json({
message: 'Hello world!',
requestIp: server.requestIP(request),
});
});The ApiResponseBuilder
Instead of manually creating new Response(...) objects, Ecopages provides the ApiResponseBuilder via context.response. This utility simplifies response creation with a fluent API.
Basic Usage
app.get('/api/data', async ({ response }) => {
const data = { message: 'Here is your data' };
// Automatically sets Content-Type to application/json
return response.json(data);
});
app.get('/api/plain', async ({ response }) => {
// Automatically sets Content-Type to text/plain
return response.text('Plain text response.');
});
app.get('/api/html-page', async ({ response }) => {
// Automatically sets Content-Type to text/html
return response.html('<h1>Hello HTML</h1>');
});Chaining Methods
You can chain methods to customize the response status and headers before sending the body.
app.post('/api/create', async ({ response }) => {
// ... creation logic ...
const newItem = { id: 123, name: 'New Item' };
return response
.status(201) // Set status to 201 Created
.headers({ 'X-Custom-Header': 'CreatedValue' }) // Add custom headers
.json(newItem); // Send JSON body
});Available Methods
.status(code: number): Sets the HTTP status code (e.g.,response.status(404))..headers(headersInit: HeadersInit): Adds or merges headers (e.g.,response.headers({ 'Cache-Control': 'no-cache' }))..json(data: any): Sends a JSON response. SetsContent-Type: application/json..text(data: string): Sends a plain text response. SetsContent-Type: text/plain..html(data: string): Sends an HTML response. SetsContent-Type: text/html..redirect(url: string, explicitStatus?: number): Sends a redirect response. Sets theLocationheader. Defaults to status 302 if not set via.status()orexplicitStatus..error(data: string | object, explicitStatus?: number): Sends an error response. Defaults to status 500. Ifdatais an object, sends JSON; otherwise, sends text.
app.get('/api/old-path', async ({ response }) => {
// Permanent redirect (301)
return response.status(301).redirect('/api/new-path');
});
app.get('/api/find/:id', async ({ params, response }) => {
const { id } = params;
const item = // ... find item logic ...
if (!item) {
// Send a 404 error with a JSON body
return response.error({ message: `Item ${id} not found` }, 404);
}
return response.json(item);
});Using the ApiResponseBuilder makes your API handler code cleaner, more readable, and less prone to errors compared to manually constructing Response objects.
WebSocket Handlers
Register WebSocket routes on the app before calling start(). Ecopages provides a runtime-agnostic WebSocket API (see WebSockets).
import { createApp } from '@ecopages/core/create-app';
import appConfig from './eco.config';
const app = await createApp({ appConfig });
app.websocket('/ws/echo', {
onMessage(socket, message) {
if (message.kind === 'text') {
socket.send(message.text);
}
},
});
await app.start();Route Groups
Use app.group() to organize related routes under a shared prefix with optional middleware. This is useful for sections like admin panels or authenticated APIs.
import { createApp } from '@ecopages/core/create-app';
import * as admin from './handlers/admin';
import { authMiddleware } from './middleware/auth';
const app = await createApp({ appConfig });
app.group(
'/admin',
(r) => {
r.get('/', admin.list);
r.get('/new', admin.newPost);
r.post('/posts', admin.createPost);
r.get('/posts/:id', admin.editPost);
r.post('/posts/:id', admin.updatePost);
r.post('/posts/:id/delete', admin.deletePost);
r.post('/upload', admin.uploadImage);
},
{
middleware: [authMiddleware],
},
);
await app.start();The group registration:
- Prepends the prefix to each route (
/becomes/admin,/posts/:idbecomes/admin/posts/:id) - Applies middleware to all routes in the group
- The middleware context is available in all handlers
Extracted handlers
For handlers in separate files, use defineGet, definePost, and the other method helpers. They wrap defineApiHandler and fix the HTTP verb for you. Register prebuilt handlers with app.add(handler).
Use inline app.get(path, handler) and app.post(path, handler) only when defining routes directly in app.ts, as shown above.
Optional response shortcuts — json, html, and redirect from @ecopages/core — share the same body emission path as context.response.json() and friends.
See Define Handlers for defineApiHandler, defineGroupHandler, schemas, middleware, and grouped routes.