Skip to content
Chapter 46Lesson 5

Quiz - Route handlers and API contracts

Quiz progress

0 / 0

You are deciding which of these endpoints stay Server Actions and which earn a route.ts. Select every case that should be a route handler.

A Stripe webhook POSTs a signed event to a fixed URL on your app.
The dashboard’s “edit invoice” form submits its changes.
An in-app page needs to read a list of invoices to render a table.
The iOS app fetches the signed-in user’s invoice list over HTTP.

Your handler guards the request body with safeParse (branch, never throw). For the response, the lesson says to pass your data through the schema with parse — which throws:

return NextResponse.json(invoiceResponseSchema.parse(data));

Why is the throwing parse the right call on the way out?

Outbound data is something your own code built, so a shape mismatch is a server bug — it should throw, surface as a 500, and the schema acts as an allowlist that keeps stray DB columns off the wire.
parse is faster than safeParse, and response serialization is the hot path where the extra speed matters most.
The response is trusted, so validation is purely documentation — parse versus safeParse makes no functional difference here.

A teammate’s PR adds POST /api/invoices/[id]/status whose body is { "status": "cancelled" } — it just sets the status field to the given value. What should the reviewer say?

Reject it — setting a field to a value is a state-diff, so it’s a PATCH on the invoice resource. POST carrying { status } is a PATCH wearing a POST costume.
Approve it — any operation that changes the database is a side effect, and side effects are exactly what POST is for.
Reject it — a status change has consequences (it might email the customer), so it must be a PUT with the invoice’s full body.

A list handler reads ?status=sent&status=overdue and builds the object it hands to Zod like this:

const raw = Object.fromEntries(searchParams);
return listInvoicesQuerySchema.safeParse(raw);

What goes wrong, and what’s the fix?

Object.fromEntries keeps only the last value of a repeated key, so sent is silently dropped. Read multi-valued keys with searchParams.getAll('status') and let that overwrite the collapsed value.
Object.fromEntries throws on a repeated key, returning a 500. Wrap the call in a try/catch and fall back to the first value.
Nothing — Object.fromEntries returns status as an array of both values, so the schema sees ['sent', 'overdue'] correctly.

Quiz complete

Score by topic