█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Programming Languages/REST APIs/Request Validation and Error Responses
50 minIntermediate

Request Validation and Error Responses

After this lesson, you will be able to: Use Zod (or similar) for input validation; return consistent error response shapes; distinguish validation vs server errors.

Inconsistent error responses are the #1 frustration when consuming an API. A consistent shape lets clients build one error handler that works everywhere.

Prerequisites:Authentication in REST APIs

Zod for input validation

TypeScript-first schema validation. The standard.

python
// npm i zod
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(13).max(120).optional(),
role: z.enum(['student', 'tutor', 'admin']).default('student'),
});
// In your route handler
app.post('/users', async (req, res) => {
const parsed = createUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(422).json({
error: 'ValidationError',
message: 'Invalid input',
details: parsed.error.issues, // array of { path, message, code }
});
}
const user = await db.users.create(parsed.data);
res.status(201).location(`/users/${user.id}`).json(user);
});

Consistent error response shape

Pick one shape; use it everywhere.

css
// One shape for ALL errors, clients can rely on it
{
"error": "ValidationError", // machine-readable code
"message": "Email is required", // human-readable summary
"details": [ // optional structured details
{ "field": "email", "code": "required" },
{ "field": "age", "code": "min", "min": 13 }
],
"requestId": "req_abc123" // for support / debugging
}
// Use the same shape for 4xx and 5xx; only the status code differs.
// Stripe + GitHub + Slack all do variants of this.

Validation vs server errors

Same envelope; different status.

tsx
// 400, malformed (bad JSON, missing required field)
// 422, well-formed but business-invalid (email already taken)
// 500, your bug
// 503, depends on something else (DB down)
// 4xx: include details client can show the user
// 5xx: log everything internally; return a generic message + requestId
// Internal
console.error({ requestId, route: req.path, error: err.stack });
// External
return res.status(500).json({
error: 'InternalError',
message: 'Something went wrong on our end.',
requestId,
});

💡 Validate at the edge

Validate request bodies + params + query strings AT the route handler, before business logic touches them. Inside business logic, trust the data. Treat un-validated input the way a Pentester would: malicious, malformed, attacker-controlled.

Error handling middleware (Express)

Catch-all so you don't repeat yourself.

python
import { ZodError } from 'zod';
app.use((err, req, res, next) => {
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
if (err instanceof ZodError) {
return res.status(422).json({
error: 'ValidationError',
message: 'Invalid input',
details: err.issues.map(i => ({ field: i.path.join('.'), code: i.code, message: i.message })),
requestId,
});
}
if (err.statusCode === 401) {
return res.status(401).json({ error: 'Unauthorized', message: 'Please sign in', requestId });
}
// Default 500
console.error({ requestId, err });
res.status(500).json({ error: 'InternalError', message: 'Internal server error', requestId });
});

Common mistakes

Returning 200 with `{error: '...'}` (clients can't tell success from failure without parsing). Different error shapes across endpoints. Leaking stack traces in 500 responses (information disclosure). Validating in business logic instead of at the edge (defensive code spreads everywhere).

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Authentication in REST APIs
Back to REST APIs
Documentation: OpenAPI and Scalar→