Episode 2 — React Frontend Architecture NextJS / 2.20 — Building APIs with NextJS
2.20 — Interview Questions: Building APIs with Next.js
Maps to 2.20.a (file conventions), 2.20.b (verbs & parsing), 2.20.c (DB + runtimes), 2.20.d (errors & contracts).
Beginner
Q1. How do you create an API endpoint in the Next.js App Router?
Model answer: I add a file named route.ts under app/api/... matching the URL I want. I export async functions named after HTTP verbs — for example export async function GET() for reads and export async function POST(request: Request) for creates. Each handler returns a Response or NextResponse. The public path is determined by the folder path under app/.
Q2. What is the difference between pages/api and app/api/.../route.ts?
Model answer: pages/api is the Pages Router pattern using a default export handler and Node-style NextApiRequest/NextApiResponse. The App Router uses route.ts with separate exports per HTTP method and the standard Web Request/Response APIs. New work should prefer route.ts unless the codebase is still on Pages Router.
Q3. What status code should GET /api/users/unknown-id return?
Model answer: 404 Not Found if the resource genuinely does not exist. I return a small JSON error body if my API convention includes errors on 404s — for example { "error": { "code": "NOT_FOUND" } } — so clients can branch programmatically.
Q4. What is NextRequest used for vs plain Request?
Model answer: NextRequest extends Request with helpers like nextUrl (parsed URL + searchParams) and cookie helpers—ergonomic for Route Handlers. Underneath it is still the Web Request model.
Intermediate
Q5. How do you implement GET, POST, PUT, PATCH, and DELETE in one resource file?
Model answer: In app/api/users/[id]/route.ts I export GET, PUT, PATCH, and DELETE as separate async functions. Each reads params for the id, parses the body where relevant, calls a shared service layer, and returns the appropriate status — for example 204 for successful delete with no body, 201 for create on the collection route, 200 or 204 for updates depending on whether I return the entity.
Q6. How do you read query parameters and JSON body?
Model answer: For query strings I use request.nextUrl.searchParams.get('key') on a NextRequest, or construct URL from request.url. For JSON I await request.json() inside try/catch and return 400 if the body is not valid JSON. I never trust the body without schema validation — typically Zod — before touching the database.
Q7. How do you connect a Route Handler to a database without exhausting connections on Vercel?
Model answer: Serverless scales by concurrency, so opening a brand-new TCP connection per request can hit database limits. I follow my ORM’s recommended pattern: singleton Prisma client with a pooled connection string or Prisma Accelerate/Data Proxy, or for MongoDB a cached connection in development and Atlas with pooling in production. I also keep handlers thin and reuse a shared dbConnect or Prisma module.
Q8. In Next 15+, how do you access params from a dynamic route.ts?
Model answer: Dynamic route params are async in newer App Router versions — I await context.params (exact signature per pinned docs) before reading id. Treating params as synchronous is a common migration footgun.
Q9. How do you implement idempotent PUT vs partial PATCH?
Model answer: PUT replaces the entire resource representation—fields omitted may be cleared or rejected per contract (document the rule). PATCH applies a partial update—only supplied fields change. I validate both with schemas (PUT stricter) and keep rules consistent with OpenAPI if published.
Advanced
Q10. How do you structure JSON errors so clients and humans both win?
Model answer: I use a stable envelope like { "error": { "code", "message", "details?" } } where code is machine-stable (EMAIL_IN_USE), message is user-safe, and details carries field-level validation issues. I map Zod failures to 422 or 400 consistently and document which. Internally I log stack traces and a request id, never returned to clients.
Q11. When would you not use a Route Handler for a mutation?
Model answer: For same-app form submissions where I do not need a public HTTP contract, Server Actions can reduce boilerplate. I still use Route Handlers for webhooks, mobile clients, third-party integrations, and anytime I need explicit REST documentation or unusual HTTP features.
Q12. Edge runtime vs Node for DB-backed APIs?
Model answer: Node is the safe default for traditional SQL/ORM drivers and long-standing TCP patterns. Edge is attractive for latency-sensitive read-through caches or JWT verification at the edge, but database clients must be edge-compatible. I verify the stack before moving DB I/O to edge; otherwise I keep database access on Node and optionally use edge middleware for lighter tasks.
Q13. How do you add CORS safely for browser clients hitting your Route Handler?
Model answer: Implement OPTIONS for preflight when needed, echo allowlisted Origin values (never *) when credentials are involved, set Access-Control-Allow-Methods narrowly, and keep CSRF implications in mind for cookie-authenticated APIs. Prefer same-origin + Server Actions for first-party apps when possible.
Quick-Fire Table
| # | Question | One-Line Answer |
|---|---|---|
| 1 | App Router API file name? | route.ts |
| 2 | Export name for listing users with HTTP read? | GET |
| 3 | Class for JSON helpers + cookies? | NextResponse |
| 4 | Read ?page=2 from Request? | nextUrl.searchParams.get('page') on NextRequest |
| 5 | Status for duplicate unique email on create? | 409 Conflict |
| 6 | Status for successful delete, no body? | 204 No Content |
| 7 | Malformed JSON body? | 400 Bad Request |
| 8 | Wrong verb with no export? | 405 Method Not Allowed (Next default) |
| 9 | Secret env var prefix to avoid? | Never NEXT_PUBLIC_ |
| 10 | Webhook from Stripe belongs in? | Route Handler + signature verification |
Answers at a glance (2.20.a → 2.20.d)
| Lesson | One sentence |
|---|---|
| 2.20.a — Conventions | Folders under app/api map to URLs; each route.ts exports HTTP verbs using Web Request/Response. |
| 2.20.b — Verbs | Parse query/body carefully; choose status codes intentionally; document idempotency semantics for writes. |
| 2.20.c — Databases | Thin handlers call services; use pooling/serverless-safe clients; default Node for classic SQL drivers. |
| 2.20.d — Errors | Stable machine codes + safe human messages; rich logs only server-side; include request ids in observability. |