Find and fix weaknesses
Validate at the boundary
Treat external data as untrusted, parse it into a known shape, and enforce authorization and invariants before use.
Every request, file, webhook, database record, and third-party response can be malformed or hostile. Validate at the boundary: parse external data into a known shape before it gains any authority inside your code.
The order matters. Validation happens first, at the edge, once. Code past the boundary should receive typed, bounded, checked values — never the raw request.
What to check
For each field: type, length, range, format, and allowed values. A schema library makes this declarative:
import { z } from 'zod'
const CreateNote = z.object({
title: z.string().min(1).max(200),
body: z.string().max(50_000),
visibility: z.enum(['private', 'shared']),
}).strict() // reject unknown fields
const result = CreateNote.safeParse(req.body)
if (!result.success) return res.status(400).json({ error: 'invalid input' })
Reject unknown fields when they create risk. The classic failure is a client sending role: "admin" and a careless spread operator copying it straight into the database.
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"ok","body":"hi","visibility":"private","role":"admin"}'
# 400 — the unknown field bounced at the boundary
Valid is not the same as safe
Here is the gap validation fills. A create-note endpoint accepts a valid string title with five million characters. The JSON parses fine — it is a string, after all. But the request consumes memory, bloats every log line that echoes it, and fills logs before the database rejects it. Type-correct, and still a denial-of-service vector. The max(200) bound kills it at the door.
Also know what validation does not do. It does not replace safe parameterized queries, output encoding, or authorization; each solves a different problem. A perfectly validated note title can still belong to another user, and a valid string can still break an HTML page that renders it unescaped.
Strictness meets rollouts
A strict schema may reject a new client field during a rollout: the mobile app v2 sends tags, the server does not know it yet, and every v2 request bounces. That is not a reason to accept every unknown value forever. Version the contract deliberately — add the field to the schema before the client ships.
Exercise one boundary with five requests: valid, missing field, wrong type, oversized, and extra field. Save the response and the database state for each, including proof that rejected input caused no write. A 400 that still inserted a row is two bugs, not one.
Lesson completed