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

PropertyTypeDescription
req.bodyanyParsed JSON body (or raw string). Up to 10 MB.
req.paramsRecord<string, string | string[]>Dynamic path segments
req.queryRecord<string, string>URL search params
req.methodstringHTTP method (GET, POST, …)
req.headersIncomingHttpHeadersRaw request headers

Response object

MethodDescription
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.