---
title: "API Handlers"
description: "Define API endpoints with typed handlers inside your Ecopages application."
order: 2
---

# 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.).

```typescript
// 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.

```typescript
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`.

```typescript
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., `Request` for Bun and Node).
	- Standard `Request` properties like `headers`, `method`, `url`, etc.
	- Methods like `json()`, `text()`, `formData()` to parse the request body.
- `params`: Route parameters parsed from the path (e.g., `:id` in `/api/users/:id`).
- `response`: An instance of `ApiResponseBuilder`, a utility for constructing `Response` objects fluently.
- `server`: The underlying server instance (e.g., Bun's server object), which exposes advanced utilities such as `requestIP`, `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 like `cache`.

### 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:

```typescript
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

```typescript
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.

```typescript
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. Sets `Content-Type: application/json`.
- `.text(data: string)`: Sends a plain text response. Sets `Content-Type: text/plain`.
- `.html(data: string)`: Sends an HTML response. Sets `Content-Type: text/html`.
- `.redirect(url: string, explicitStatus?: number)`: Sends a redirect response. Sets the `Location` header. Defaults to status 302 if not set via `.status()` or `explicitStatus`.
- `.error(data: string | object, explicitStatus?: number)`: Sends an error response. Defaults to status 500. If `data` is an object, sends JSON; otherwise, sends text.

```typescript
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](/docs/server/websockets)).

```typescript
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.

```typescript
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/:id` becomes `/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](/docs/server/define-handlers) for `defineApiHandler`, `defineGroupHandler`, schemas, middleware, and grouped routes.
