Input and resource controls
Validate every request shape
Parse parameters, headers, bodies, and content types into bounded application values before business logic uses them.
Valid JSON only proves the body is JSON. It says nothing about whether the values inside are safe for your operation. A perfectly parseable body can carry 50,000 line items, or a negative total.
So we validate the shape, not just the syntax.
Validate structure and bounds
Check types, lengths, ranges, formats, nesting depth, array size, and allowed fields. A schema library keeps those rules declarative, and declarative rules are easy to test.
Here’s the invoice schema with Zod:
import { z } from 'zod'
const createInvoice = z.object({
customerId: z.string().regex(/^cus_[a-z0-9]{12}$/),
items: z.array(z.object({
sku: z.string().max(64),
quantity: z.number().int().min(1).max(999),
})).min(1).max(100),
total: z.number().positive().max(1_000_000),
}).strict()
Every field has a bound. items needs between 1 and 100 entries. quantity is a positive integer under 1000. total can’t be negative or absurd.
.strict() rejects unknown fields. For input that carries authority, like this one, rejecting unknowns is the safer choice. For harmless data from a provider that adds fields over time, a tolerant reader can make sense. Know which case you’re in.
Reject unsupported content types too, and ambiguous duplicates. Two total fields in one body. A text/plain request your framework helpfully parses as JSON anyway. Both should fail.
Verify rejection at the boundary
Test each limit exactly, then one step beyond it:
curl -i -X POST https://api.example.com/invoices \
-H "Content-Type: application/json" \
-d '{"customerId":"cus_ab12cd34ef56","items":[],"total":-500}'
# HTTP/1.1 400 Bad Request
# {"error":"items: at least 1 entry; total: must be positive"}
Then open the database and confirm no invoice row was created. A 400 that still wrote a partial record is worse than no validation at all, because from the outside it looks safe.
Validate again off the HTTP path
HTTP is not the only way data enters your system. When a message arrives from a queue or from a third party, validate it again.
Check the queue consumer in particular. An internal producer, or an old message, can bypass today’s HTTP schema entirely. A message enqueued last year under an older schema will replay against today’s assumptions, and nothing on the HTTP side will catch it.
One more detail about timing. Validate after decoding but before any expensive business work starts. Parsing 50,000 line items just to reject them at the end still costs you the parse. Put array-size limits as early as the body parser allows.
Try this on one endpoint: write down the accepted schema with its numeric, size, and nesting limits. Test each exact boundary and one value past it. Then prove that every rejected request created nothing in the database.
Lesson completed