API

How to add and customize API route handlers with typed responses, structured logging, and error patterns.

1. Existing Endpoints

Both apps include health check endpoints and data APIs out of the box.

AppEndpointDescription
WebsiteGET /api/healthHealth check
WebsiteGET /api/posts?tag=X&limit=NBlog posts with optional filtering
Case StudyGET /api/healthHealth check
Case StudyGET /api/catalog?category=X&subcategory=YCatalog items with optional filtering

2. Adding a New Route

Create a new file at src/app/api/{name}/route.ts. Use NextRequest and NextResponse for typed request/response handling. Always use the structured logger instead of console.log.

1// src/app/api/users/route.ts
2import { NextRequest, NextResponse } from "next/server";
3import { logger } from "@/server/logging/logger";
4
5export async function GET(request: NextRequest) {
6 const { searchParams } = request.nextUrl;
7 const role = searchParams.get("role");
8
9 logger.info("Fetching users", { role });
10
11 if (!role) {
12 return NextResponse.json(
13 { error: "Missing role parameter" },
14 { status: 400 },
15 );
16 }
17
18 const users = [{ id: 1, name: "Erik", role: "admin" }];
19
20 return NextResponse.json({ data: users });
21}

3. Handling POST Requests

Parse the request body with request.json(). Return typed error responses with appropriate status codes. Log all operations for observability.

1export async function POST(request: NextRequest) {
2 try {
3 const body = await request.json();
4
5 logger.info("Creating resource", { body });
6
7 // Validate required fields
8 if (!body.name) {
9 return NextResponse.json(
10 { error: "Name is required" },
11 { status: 400 },
12 );
13 }
14
15 const created = { id: Date.now(), ...body };
16
17 return NextResponse.json({ data: created }, { status: 201 });
18 } catch {
19 logger.error("Failed to create resource");
20 return NextResponse.json(
21 { error: "Internal server error" },
22 { status: 500 },
23 );
24 }
25}

4. Proxying to a Backend

When using a separate backend (FeatherJS, FastAPI), Next.js API routes act as a proxy. This keeps the backend URL private and lets you add auth headers server-side.

1// src/app/api/projects/route.ts
2export async function GET(request: NextRequest) {
3 const token = request.cookies.get("token")?.value;
4
5 const res = await fetch(`${process.env.BACKEND_URL}/projects`, {
6 headers: {
7 Authorization: `Bearer ${token}`,
8 "Content-Type": "application/json",
9 },
10 });
11
12 if (!res.ok) {
13 const error = await res.json();
14 return NextResponse.json(
15 { error: error.message },
16 { status: res.status },
17 );
18 }
19
20 const data = await res.json();
21 return NextResponse.json({ data });
22}

Conventions: Use logger.info() for all operations, logger.error() for failures. Return { error: "message" } for errors and { data: ... } for success. Always validate input at the boundary.

Built with Next.js, deployed from a couch in Indonesia.

© 2025 Erik Yuntantyo · turmerrific