Input Validation and Rejecting Bad Requests
Never trust a request body. Add real validation and a centralized error-handling middleware to your local Express API, so bad input is rejected consistently and safely everywhere.
What you'll learn
- Write a validation function that checks required fields, types, and value constraints
- Explain what a centralized error-handling middleware is and why it's better than repeating error logic in every route
- Add real request validation and centralized error handling to a local Express server
Prerequisites
Explanation
A request body is data from outside your program's control — it can be malformed, missing required fields, the wrong types, or actively malicious, regardless of what your frontend intends to send. Every field a route handler reads from req.body should be validated before it's trusted, the same discipline from this curriculum's testing courses (negative testing, schema validation) applied from the server's own implementation side rather than as an external test.
A real validation function checks each field against its actual requirements — required-ness, type, and value constraints — and collects every problem found, not just the first one, so a client fixing one issue doesn't have to resubmit repeatedly just to discover the next: { email: "email is required", progressPercent: "must be between 0 and 100" }, both reported together.
Centralized error handling solves a real duplication problem: without it, every route handler needs its own try/catch and its own logic for turning an error into a proper status code and response body — repeated dozens of times, drifting slightly out of sync with itself over time. Express supports a special error-handling middleware signature with four parameters, (err, req, res, next) — Express recognizes this specific arity and only invokes such middleware when something calls next(err) (passing an error) instead of next(). One centralized error handler, registered last, converts any error passed to it into a consistent, structured response — the same status/body shape from this course's API-testing-adjacent concepts, applied here on the server that produces those responses rather than the client testing them.
This lesson's guided local lab adds both pieces — real request validation and a centralized error-handling middleware — to the Express server from earlier in this course.
Example
A real validation function collecting ALL errors found, not just the first -- the exact shape the guided local lab's Express route will use.
function validateEnrollment(body) {
const errors = {};
if (typeof body.courseId !== "number") errors.courseId = "courseId must be a number";
if (typeof body.status !== "string" || !["active", "completed"].includes(body.status)) {
errors.status = "status must be 'active' or 'completed'";
}
return errors;
}
const errors = validateEnrollment({ courseId: "not-a-number", status: "unknown" });
console.log(errors);
console.log(Object.keys(errors).length === 0 ? "valid" : "invalid");Try it yourself
Fix the body below so it passes validation, then re-run to confirm errors is empty.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Using validateEnrollment already defined, run it on THREE bodies and store each result: resultAllInvalid (both fields wrong), resultAllValid (both fields correct: courseId 1, status 'active'), resultOneInvalid (courseId correct, status wrong).
Checks: reports both errors when both fields are invalid · reports zero errors for a fully valid body · reports only the one genuinely invalid field
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write buildErrorResponse(errors) that takes a validation errors object (like { courseId: 'msg' }) and returns { status: 400, body: { error: { code: 'VALIDATION_ERROR', fields: errors } } } if errors has any keys, or null if errors is empty (meaning no error response is needed).
Checks: builds a structured 400 response for real errors · returns null when there are no errors
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Guided local lab
Add Validation and Centralized Error Handling
Runs on your computerExtend the local Express server from earlier in this course with real request validation on POST /enrollments and a single centralized error-handling middleware that every route can rely on.
Required tools
- Node.js (20.x or 22.x LTS)
- npm (10.x (bundled with Node.js))
Setup
- Reuse the `learning-api` project from the Express setup lab earlier in this course.
- Add a new file `src/errors.js` with the AppError class below.
- Update `src/routes/enrollments.routes.js` and `src/server.js` per the starter files below.
- Restart the server with `node src/server.js`.
Project structure
learning-api/
src/
server.js (updated: registers the error handler last)
errors.js (new)
routes/
courses.routes.js
enrollments.routes.js (updated: validates input)Starter files
src/errors.js
export class AppError extends Error {
constructor(status, code, message, fields) {
super(message);
this.status = status;
this.code = code;
this.fields = fields;
}
}src/routes/enrollments.routes.js
import { Router } from "express";
import { AppError } from "../errors.js";
const router = Router();
const ENROLLMENTS = [];
function validateEnrollment(body) {
const errors = {};
if (typeof body.courseId !== "number") errors.courseId = "courseId must be a number";
if (typeof body.status !== "string" || !["active", "completed"].includes(body.status)) {
errors.status = "status must be 'active' or 'completed'";
}
return errors;
}
router.get("/", (req, res) => {
res.json(ENROLLMENTS);
});
router.post("/", (req, res, next) => {
// TODO: validate req.body with validateEnrollment. If there are errors,
// call next(new AppError(400, "VALIDATION_ERROR", "Invalid enrollment", errors))
// instead of responding directly. Otherwise create and respond 201 as before.
const enrollment = { id: ENROLLMENTS.length + 1, ...req.body };
ENROLLMENTS.push(enrollment);
res.status(201).json(enrollment);
});
export default router;src/server.js
import express from "express";
import coursesRouter from "./routes/courses.routes.js";
import enrollmentsRouter from "./routes/enrollments.routes.js";
const app = express();
app.use(express.json());
app.use("/courses", coursesRouter);
app.use("/enrollments", enrollmentsRouter);
// TODO: add a centralized error-handling middleware HERE, after all routes.
// It must have exactly 4 parameters: (err, req, res, next).
const PORT = 3001;
app.listen(PORT, () => {
console.log("Learning API listening on port " + PORT);
});Requirements
- POST /enrollments rejects an invalid body with status 400 and a structured error body naming the invalid fields
- The error-handling middleware is registered ONCE, after all routes, with exactly 4 parameters
- A valid POST /enrollments request still succeeds with 201, unaffected by the new validation
- No route handler contains its own ad hoc try/catch that duplicates the centralized error response shape
Commands to run
Start the server
node src/server.jsTest invalid input
curl -i -X POST http://localhost:3001/enrollments -H "Content-Type: application/json" -d "{}"
Expected behavior
POST /enrollments with an empty or malformed body responds 400 with { error: { code: 'VALIDATION_ERROR', fields: {...} } } naming every invalid field. A well-formed body still responds 201 as before.
Verify it yourself
curl -i -X POST http://localhost:3001/enrollments -H "Content-Type: application/json" -d "{}"Expected: HTTP 400 with a body naming both courseId and status as invalid
curl -i -X POST http://localhost:3001/enrollments -H "Content-Type: application/json" -d "{\"courseId\":1,\"status\":\"active\"}"Expected: HTTP 201 with the created enrollment, exactly as before adding validation
Troubleshooting
- The error-handling middleware never runs, even after calling next(new AppError(...)) — Confirm the error-handling middleware is registered with app.use() AFTER every route, and that it has exactly 4 parameters (err, req, res, next) — Express identifies error handlers by that specific arity.
- Calling next(err) causes the default Express error page to appear instead of the JSON response — This means the centralized handler either isn't registered, or is registered before some routes — move it to the very end of server.js, after all app.use() route registrations.
Stuck? Get a hint.
Extension challenge
Add validation to the courses route's (hypothetical) POST endpoint too, reusing the same AppError and centralized handler, to confirm the error handling genuinely works across more than one route without duplication.
When you've verified this locally, use the "Mark lesson complete" button below to record your progress.
Common mistakes
- Validating a request body's presence but not its types, letting a string silently pass through where a number was expected.
- Writing a custom try/catch and error response in every single route handler instead of a single centralized error-handling middleware.
- Registering the error-handling middleware before some routes, or giving it fewer than 4 parameters, so Express never recognizes it as an error handler.
Knowledge check
Takeaway
Validate every field of untrusted input, collecting every error found — and a single centralized error-handling middleware (registered last, with exactly 4 parameters) avoids repeating error-response logic across every route.
Summary
This lesson covered writing a real multi-field validation function and the mechanics of Express's error-handling middleware, then added both real validation and centralized error handling to a local Express server via the guided local lab.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.