Plain Function Handlers
Simple API endpoints with a default-exported Request → Response function.
Plain function handlers are the simplest way to create API routes. Ideal for single endpoints that do not need Hono middleware or nested routing.
src/app/api/hello.ts is served at /api/hello. There is no bare /api root route.Basic Handler
Export a default function that receives Request. The file path sets the route - the function name does not matter.
export default function handler(req: Request) {
return Response.json({ message: 'hello', method: req.method })
}This creates /api/hello and responds to all HTTP methods unless you branch on request.method.
Route Mapping
File structure maps directly to API paths:
| File Path | API Route |
|---|---|
| src/app/api/hello.ts | /api/hello |
| src/app/api/user.ts | /api/user |
| src/app/api/posts.ts | /api/posts |
| src/app/api/posts/[id].ts | /api/posts/:id |
| src/app/api/posts/index.ts | /api/posts |
| src/app/api/[...catch].ts | /api/* |
Handling HTTP Methods
Branch on request.method for different verbs:
export default function handler(request: Request) {
if (request.method === 'GET') {
return Response.json({ posts: [] })
}
if (request.method === 'POST') {
return Response.json({ message: 'Post created' }, { status: 201 })
}
if (request.method === 'PUT') {
return Response.json({ message: 'Post updated' })
}
if (request.method === 'DELETE') {
return Response.json({ message: 'Post deleted' })
}
return Response.json({ error: 'Method not allowed' }, { status: 405 })
}| Method | Typical Use |
|---|---|
| GET | Retrieve data |
| POST | Create new data |
| PUT | Replace existing data |
| PATCH | Partially update data |
| DELETE | Remove data |
Reading Request Data
Body, headers, and query params:
export default async function handler(request: Request) {
const body = await request.json().catch(() => null)
const userAgent = request.headers.get('User-Agent')
const url = new URL(request.url)
const page = url.searchParams.get('page')
return Response.json({
method: request.method,
body,
headers: { userAgent },
query: { page },
})
}Sending Responses
Common response patterns:
export default function handler(request: Request) {
// JSON
return Response.json({ message: 'Hello JSON' })
// Plain text
// return new Response('Hello Text', {
// headers: { 'Content-Type': 'text/plain' },
// })
// Custom status
// return Response.json({ message: 'Created' }, { status: 201 })
// Redirect
// return Response.redirect('https://example.com', 302)
}Dynamic Routes
Path params are available via the x-bini-params header:
export default async function handler(request: Request) {
const paramsHeader = request.headers.get('x-bini-params')
let params: Record<string, string> = {}
try {
params = paramsHeader ? JSON.parse(paramsHeader) : {}
} catch {
return Response.json({ error: 'Invalid params' }, { status: 400 })
}
const id = params.id
if (request.method === 'GET') {
return Response.json({ id, title: `Post ${id}` })
}
return Response.json({ error: 'Method not allowed' }, { status: 405 })
}Catch-all Routes
[...catch] handles unmatched /api/* paths:
export default function handler(request: Request) {
console.warn('Unmatched API route', { method: request.method })
return Response.json(
{
error: 'Not Found',
message: 'The requested endpoint does not exist',
},
{ status: 404, headers: { 'Cache-Control': 'no-store' } }
)
}Environment Variables
Use getEnv and requireEnv from bini-env:
import { getEnv, requireEnv } from 'bini-env'
export default function handler(request: Request) {
const apiKey = requireEnv(request as any, 'MY_API_KEY')
const debug = getEnv(request as any, 'DEBUG_MODE') ?? 'false'
const appName = getEnv(request as any, 'APP_NAME') ?? 'Bini.js'
return Response.json({ appName, debug: debug === 'true', hasKey: !!apiKey })
}| Function | Returns | Behavior |
|---|---|---|
| getEnv(ctx, key) | string | undefined | Undefined if missing - use ?? for defaults |
| requireEnv(ctx, key) | string | Throws if missing or empty |
Error Handling
Validate input and catch unexpected failures:
import { getEnv } from 'bini-env'
export default async function handler(request: Request) {
try {
const body = await request.json()
if (!body.email) {
return Response.json({ error: 'Email is required' }, { status: 400 })
}
return Response.json({ success: true })
} catch (error: any) {
const isDev = getEnv(request as any, 'NODE_ENV') === 'development'
return Response.json(
{
error: 'Internal Server Error',
...(isDev && { details: error.message }),
},
{ status: 500 }
)
}
}When to Use Plain Handlers
| Scenario | Recommendation |
|---|---|
| Single endpoint with simple logic | Plain handler |
| Quick prototypes | Plain handler |
| Simple CRUD | Plain handler |
| Multiple endpoints in one file | Use Hono |
| Need middleware | Use Hono |
| Complex routing | Use Hono |
| Large production API surface | Use Hono |
Complete Example
A small todos API with validation and method branching:
import { getEnv } from 'bini-env'
const todos: { id: string; title: string; completed: boolean }[] = []
export default async function handler(request: Request) {
const url = new URL(request.url)
const id = url.searchParams.get('id')
try {
if (request.method === 'GET' && !id) {
return Response.json(todos)
}
if (request.method === 'GET' && id) {
const todo = todos.find((t) => t.id === id)
if (!todo) {
return Response.json({ error: 'Todo not found' }, { status: 404 })
}
return Response.json(todo)
}
if (request.method === 'POST') {
const body = await request.json()
if (!body.title) {
return Response.json({ error: 'Title is required' }, { status: 400 })
}
const todo = {
id: Date.now().toString(),
title: body.title,
completed: false,
}
todos.push(todo)
return Response.json(todo, { status: 201 })
}
if (request.method === 'DELETE' && id) {
const index = todos.findIndex((t) => t.id === id)
if (index === -1) {
return Response.json({ error: 'Todo not found' }, { status: 404 })
}
todos.splice(index, 1)
return Response.json({ message: 'Todo deleted' })
}
return Response.json({ error: 'Method not allowed' }, { status: 405 })
} catch (error: any) {
const isDev = getEnv(request as any, 'NODE_ENV') === 'development'
return Response.json(
{
error: 'Internal Server Error',
...(isDev && { details: error.message }),
},
{ status: 500 }
)
}
}