Course outline

Next.js / Intermediate: authority, freshness and route experience / Lesson 7

Route Handlers and HTTP contracts

Expose a bounded server API with explicit method, input, identity and response semantics.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish route.ts from a page and use Web request/response APIs deliberately.
  • Validate asynchronous route params and preserve safe failure categories.

A Route Handler is an independent server entry point

A route.ts file defines HTTP handlers rather than rendered page UI. Export supported method functions such as GET or POST and keep page.tsx and route.ts from competing at the same route segment. A handler is reachable directly, so a protected layout does not authorize it. Use a shared server data layer for object eligibility and result projection. Server Components can usually call that layer without making an internal HTTP request back to this handler. Keep public API clients, browser fetches and server rendering as separate consumers of the same domain contract rather than assuming they share one authentication context.

Await params and return an intentional response

The fragment represents app/api/courses/[id]/route.ts. It awaits params, derives current authority through a reviewed helper and asks a policy-aware catalog for a card. Response.json builds the selected public shape and status. The helpers, imports and runtime integration are assumed; no request is sent. A missing card uses the declared not-found policy, while unexpected source failures require safe error translation rather than pretending that the object is absent. In the primary mode, GET Route Handlers are not cached by default; any opt-in static behavior must be a separate reviewed public-data decision.

typescript Illustrative policy-aware GET handler
export async function GET(request:Request,{params}:{params:Promise<{id:string}>}) {
  const {id}=await params;
  const principal=await principalFor(request);
  const card=await catalog.visibleCard(principal,id);
  if (!card) return Response.json({code:"COURSE_NOT_FOUND"},{status:404});
  return Response.json({id:card.id,title:card.title});
}

The handler uses Web-style APIs and asynchronous params. Authentication, source access and actual HTTP behavior remain unexecuted.

Body parsing is not a domain schema

request.json and request.formData parse supported body forms, but the resulting values still need structural and business validation. A body stream has consumption and size constraints; do not read it repeatedly or assume a proxy’s limit automatically bounds application buffering. Signed callbacks can require original bytes before parsing, with replay and sender verification under the specific protocol. Query parameters can repeat, so choose an explicit duplicate policy rather than silently relying on the first value. Treat content type, syntax, domain shape and authorization as distinct rejection stages with a safe public contract.

Reject duplicate values under a declared contract

The local parser accepts zero or one view parameter and permits only summary or detail. It rejects repetition instead of letting two downstream consumers interpret different values. The model uses URLSearchParams on a synthetic query string and opens no connection. Its exact vocabulary and duplicate rule belong to this lab API, not every web endpoint. A real handler should produce a narrow domain input and verify other required parameters separately. This test does not validate Next routing, request-body handling or the caller’s eligibility to see the detailed course representation.

javascript Complete offline query-cardinality check
function viewFrom(query) {
  const values=new URLSearchParams(query).getAll("view");
  if (values.length > 1) throw new Error("DUPLICATE_VIEW");
  const view=values[0] ?? "summary";
  if (!["summary","detail"].includes(view)) throw new Error("INVALID_VIEW");
  return view;
}
console.assert(viewFrom("") === "summary");
console.assert(viewFrom("view=detail") === "detail");
let rejected=false;try {viewFrom("view=summary&view=detail");} catch {rejected=true;}
console.assert(rejected);

Absence defaults to summary, detail is accepted and duplicate view is rejected. No Route Handler is invoked.

Keep transport, cache and write outcomes distinct

Choose status, media type, cache audience and error body deliberately. A 204 has no content body, and sending headers does not prove complete client delivery. Streaming requires a late-failure and cleanup policy; a handler cannot replace already-sent bytes with a new JSON error. A write can commit before its response is lost, so retries need operation identity and reconciliation. Test method handling, unauthorized scope, malformed body and source failure through an actual isolated HTTP integration when authorized. This course executes the query parser only and does not start a handler, parse a live body or verify framework status/caching behavior.

Common errors and fixes

A layout gate is assumed to protect the API

Why it happens
UI composition was mistaken for endpoint authorization.
How to fix it
Authenticate and authorize inside the handler and shared data layer.
Prevent it next time
Test direct requests under an ineligible principal.

A second body read is empty or fails

Why it happens
A consumable stream was treated as reusable ordinary data.
How to fix it
Choose one body-reading owner and pass validated values onward.
Prevent it next time
Review raw-body signature and parser ordering explicitly.

Duplicate query values produce inconsistent behavior

Why it happens
Different layers interpreted first, last or all values differently.
How to fix it
Define and enforce one cardinality rule.
Prevent it next time
Retain duplicate-parameter fixtures.

Check your understanding

1. Does a GET handler’s uncached default mean every source it reads is fresh?

Show worked solution

No. The handler, its fetch calls, a domain cache and downstream HTTP caches are distinct layers. The helper may return an intentionally cached or stale value. Identify the actual representation and audience at each layer and test source freshness separately from handler response caching.

2. Why reject duplicate view values rather than trusting get?

Show worked solution

This API chooses one unambiguous input meaning. If another layer reads all values or the last value, silently taking the first can create disagreement. Other APIs may permit lists, but they should declare that shape explicitly. Cardinality validation establishes input meaning, not permission to read the selected view.

Key takeaways

  • Route Handlers require independent server-entry protection.
  • Input cardinality, body ownership and cache audience are explicit contracts.

Official references