Validation & Transformation
Three layers of validation. Actionable error messages. Trust nothing, sanitize early.
Why Validate?
User input is untrusted by default. A missing field causes a crash. A negative price causes financial loss. An SQL injection string destroys your database. XSS payloads run code in other users' browsers.
Validation is your first line of defense. It also communicates intent — your schema is documentation of what the API expects.
Validate as early as possible in the request lifecycle — before business logic runs. Return 400 errors immediately with clear messages.
Types of Validation
There are two useful ways to slice validation: by what you're checking, and by where in the request flow it happens.
By what you're checking — the three classic categories:
1. Type validation — "Is this value the right shape?"
Is age a number? Is name a string? Is tags an array? Libraries like Zod (TypeScript), Pydantic (Python), and Joi (JS) excel at this — they give you a schema and reject anything that doesn't match.
2. Semantic validation — "Does this value make sense in context?"
Is the date of birth in the past? Is age between 1 and 120? Is the price positive? The format is fine; the meaning has to be too.
3. Syntactic validation — "Does this string match the expected pattern?"
Is this a valid email format? Is this a valid phone number? Is this a UUID? Format-level checks beyond just type.
By where it lives in the request flow:
Structural validation (handler/controller layer):
• Is the body valid JSON?
• Are required fields present?
• Do types match?
Business validation (service/logic layer):
• Does the user have enough balance?
• Is the discount code still valid?
• Can a user have more than 5 free accounts?
The first three (Type / Semantic / Syntactic) typically happen at the boundary — middleware or controller, before business logic runs. Business validation happens inside the service layer because it requires reading from the database and applying domain rules.
Input Transformation
Transformation cleans and normalizes input before it reaches business logic:
- Trim whitespace from strings: " alice@test.com " → "alice@test.com"
- Lowercase emails: "ALICE@TEST.COM" → "alice@test.com"
- Parse dates: "2024-01-15" → Date object
- Convert types: "42" → 42 (string to number from query params)
- Strip dangerous characters: sanitize HTML to prevent XSS
- Default values: if limit is undefined → 20
Transform after validation, not before. Validate the raw input, then transform it into the shape you need.
Error Messages
Good validation errors are actionable and specific:
Bad: { "error": "Invalid input" }
Good: {
"error": "Validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be 18 or older" },
{ "field": "name", "message": "Required field is missing" }
]
}
Rules:
• Return ALL validation errors at once (not one at a time)
• Name the field that failed
• Say what the constraint is, not just that it failed
• Don't leak internal implementation details (no stack traces)
• Use consistent error response shapes across your entire API
The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.