Write a real REST endpoint in Next.js. Create a Route Handler in app/api, return JSON with Response.json, read a dynamic [id] from the URL, and handle GET and POST.
In the Next.js App Router you build an API with a "Route Handler" — a file named route.ts inside the app/api folder. You export a function named after the HTTP method (GET, POST, ...), and Next.js calls it when a request comes in. Response.json() is a built-in helper that turns a JavaScript value into a JSON response with the right Content-Type header. The folder path becomes the URL: app/api/users/route.ts answers /api/users.
// app/api/users/route.ts
const users = [
{ id: '42', name: 'Ada' },
{ id: '43', name: 'Alan' },
]
// Handles GET /api/users
export async function GET() {
// Response.json() sets Content-Type: application/json for you.
return Response.json({ data: users })
}To handle /api/users/42 you put the changing part of the path in square brackets: the folder app/api/users/[id]. Next.js hands your function a context object whose params holds that value. In this version of Next.js params is async, so you await it. The RouteContext type is generated for you from the folder path, so the id is fully typed. Return a 404 when nothing matches — set the status as the second argument to Response.json().
// app/api/users/[id]/route.ts
import type { NextRequest } from 'next/server'
const users = [{ id: '42', name: 'Ada' }]
// Handles GET /api/users/:id
export async function GET(
_req: NextRequest,
ctx: RouteContext<'/api/users/[id]'>
) {
const { id } = await ctx.params // params is async in this Next.js
const user = users.find((u) => u.id === id)
if (!user) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
return Response.json({ data: user })
}A POST carries data in its body. You read it with await request.json(), which parses the incoming JSON into a JavaScript object. Create the resource, then answer with 201 Created and the thing you just made so the client gets its new id without a second request. Wrap the parse in try/catch — a client can always send broken JSON, and that should be a clean 400, not a crashed 500. (The next lesson adds real field validation on top of this.)
// app/api/users/route.ts
import type { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
let body: { name?: string }
try {
body = await request.json() // parse the JSON the client sent
} catch {
return Response.json({ error: 'Body must be valid JSON' }, { status: 400 })
}
if (!body.name) {
return Response.json({ error: 'name is required' }, { status: 400 })
}
const user = { id: crypto.randomUUID(), name: body.name }
// ...save user to your database here...
// 201 Created + the new resource, so the client gets its id back.
return Response.json({ data: user }, { status: 201 })
}