API Routes
Export named HTTP method handlers from files in server/ and they become fully typed API endpoints.
Creating a route
Create a .ts file in server/ and export named functions matching HTTP methods. The file path maps to the URL the same way pages do:
server/users/index.ts → GET /users, POST /userstypescript
import type { IncomingMessage, ServerResponse } from 'node:http'
export async function GET(req: IncomingMessage, res: ServerResponse) {
const users = await db.getUsers()
res.json(users)
}
export async function POST(req: IncomingMessage, res: ServerResponse) {
const user = await db.createUser(req.body)
res.json(user, 201)
}Dynamic API routes
Use [param] in the filename. Route params land in req.params:
server/users/[id].ts → /users/:idtypescript
import type { IncomingMessage, ServerResponse } from 'node:http'
export async function GET(req: IncomingMessage, res: ServerResponse) {
const { id } = req.params as { id: string }
const user = await db.getUser(id)
if (!user) {
res.json({ error: 'Not found' }, 404)
return
}
res.json(user)
}
export async function PUT(req: IncomingMessage, res: ServerResponse) {
const { id } = req.params as { id: string }
const updated = await db.updateUser(id, req.body)
res.json(updated)
}
export async function DELETE(req: IncomingMessage, res: ServerResponse) {
await db.deleteUser(req.params.id as string)
res.status(204).end()
}Query string parameters
Query params land in req.query as plain strings alongside req.params:
server/products/[id].ts → /products/:id?include=reviewstypescript
export async function GET(req: IncomingMessage, res: ServerResponse) {
const { id } = req.params as { id: string }
const { include } = req.query // e.g. ?include=reviews
const product = await db.getProduct(id)
if (include === 'reviews') {
product.reviews = await db.getReviews(id)
}
res.json(product)
}Request object
| Property | Type | Description |
|---|---|---|
| req.body | any | Parsed JSON body (or raw string). Up to 10 MB. |
| req.params | Record<string, string | string[]> | Dynamic path segments |
| req.query | Record<string, string> | URL search params |
| req.method | string | HTTP method (GET, POST, …) |
| req.headers | IncomingHttpHeaders | Raw request headers |
Response object
| Method | Description |
|---|---|
| res.json(data, status?) | Send JSON. Default status 200. |
| res.status(code) | Set status code, returns res for chaining. |
| res.setHeader(name, value) | Set a response header. |
| res.end(body?) | Send a raw response and close. |
Calling API logic from pages
Because pages are server components, you can import and call your database layer directly — skipping the HTTP round-trip entirely:
app/pages/users.tsxtypescript
import { getUsers } from '../../lib/db'
export default async function UsersPage() {
const users = await getUsers() // no fetch() needed
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
)
}API routes are for your frontend clients Server-rendered pages can call database code directly. API routes are most useful for client components making
fetch() calls, or for third-party integrations.