2 · Data and boundaries · 30 MIN
Route handlers and validated input
An endpoint owns its input and output contract.
A route handler receives the Web Request API and returns a Response. Parse JSON inside a try block because malformed input is an expected client failure. Zod checks the runtime shape instead of relying on a TypeScript assertion. The example previews a title and does not persist anything, so it deliberately does not pretend to be an authenticated create endpoint. Before adding writes, enforce session ownership, same-origin protection where relevant, rate limits and bounded body reads on the server.
Verify trimming and both length boundaries.
Read the example
// app/api/preview/route.ts — install zod in the example project
import {z} from 'zod';
const Input=z.object({title:z.string().trim().min(3).max(80)}).strict();
export async function POST(request:Request) {
let raw:unknown;
try {raw=await request.json()} catch {return Response.json({error:'Invalid JSON'},{status:400})}
const parsed=Input.safeParse(raw);
if(!parsed.success)return Response.json({error:'Invalid title'},{status:400});
return Response.json({preview:parsed.data.title},{headers:{'Cache-Control':'no-store'}});
}Check the expected output
POST {"title":" React "} returns {"preview":"React"}; malformed JSON or a short title returns 400.Your challenge
Add a streaming request-body byte limit and tests for malformed JSON, unknown fields and oversized input before adapting this preview to a write endpoint.
Solution cost: O(b) parsing for a bounded b-byte body. time · O(b) parsed request data. space
Common trap
A TypeScript cast does not validate JSON; Content-Length alone is not a reliable body limit.
Study the project implementation
// app/api/preview/route.ts — install zod in the example project
import {z} from 'zod';
const Input=z.object({title:z.string().trim().min(3).max(80)}).strict();
export async function POST(request:Request) {
let raw:unknown;
try {raw=await request.json()} catch {return Response.json({error:'Invalid JSON'},{status:400})}
const parsed=Input.safeParse(raw);
if(!parsed.success)return Response.json({error:'Invalid title'},{status:400});
return Response.json({preview:parsed.data.title},{headers:{'Cache-Control':'no-store'}});
}Further reading: Official documentation
Next lesson: Loading states and recoverable errors →