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