4.4Validation, error handling & tests (Jest)
The essentials in 30 seconds
- Never trust inputs: validate (Zod/Joi) before processing, reject with a clear 400 — against corrupt data, bugs and flaws.
- Centralise errors: Express middleware (err, req, res, next) last, try/catch on awaits, expected 404/400 vs unexpected 500.
- Salim's reflex: test with Jest (unit + route) and re-run after each change — the non-regression that sets you free.
Running case — Salim hardens before going live
Salim's API (4.1-4.3) works in demo, but a seller creates a product without a name, a route crashes on an unknown id and nobody knows what to test before each release. Salim adds a Zod schema rejecting invalid bodies with 400, a centralised error middleware answering cleanly without leaking details, then writes his first Jest tests: one function tested in isolation, one route tested end to end. At the next change, he re-runs the suite in seconds. Last milestone of the thread: from prototype to professional application.
Last episode of the 4.1-4.3 case — Salim closes the loop: build, secure, animate, harden.
1. Input validation
Reminder from 4.2: never trust inputs. Before processing client data (bodies, parameters), check it has the expected shape: required fields present, right types, constraints respected (valid email, maximum length). If invalid: reject with 400 and a clear message, before any processing. Libraries: Zod (modern, excellent with TypeScript — one schema that validates and types) or Joi (proven). Validation protects against corrupt data, bugs and some flaws — essential for any external input.
2. Centralised error handling
In production, errors happen: database unavailable, resource not found, failed operation. In Express, the pattern is a centralised error-handling middleware — a special 4-parameter function (err, req, res, next), placed last: errors are directed there and it returns a consistent response (right code, clear message, no sensitive details). Wrap await calls in try/catch (or a wrapper) and distinguish expected errors (404, 400) from unexpected ones (500).
3. Automated tests with Jest
Rather than checking by hand (tedious, incomplete, non-repeatable), you write code that tests code. Jest is the most popular framework in Node: unit (one isolated function), integration (several parts, one route with its database), end to end. A test checks that an input gives the expected output (expect(resultat).toBe(attendu)). Benefits: confidence, non-regression (re-run after each change), documentation of behaviour, better design (testable code is better structured). Validation + errors + tests: what separates a prototype from a professional app.
🔎 Concrete example — 🔎 Concrete example — Validate then test
Salim's two moves on product creation:
Schema: { nom: non-empty string, prix: positive number }
→ invalid body: 400 { erreur: 'name required, positive price expected' }
Test: POST /produits without name → expect 400
Test: total(panier) function → expect(total).toBe(300)
Reading: the schema rejects before processing, the test locks the behaviour.
Lesson: clear 400 for the client, green tests for the team.
Section vocabulary
- Input validation
- Checking received data has the expected shape (fields, types, constraints) before processing; rejecting with 400 if invalid. Tools: Zod, Joi.
Simple example: Salim's schema refuses a product without a name before any write.
What it is not: processing req.body directly without any check.
Reflex: no external input touches data without a schema.
- Error middleware
- Special Express function (err, req, res, next) placed last, centralising errors and returning a consistent response.
Simple example: At Salim's, every error ends there: right code, clear message, no sensitive leak.
What it is not: scattered try/catch blocks each returning a different format.
Reflex: a single error format for the whole API.
- Expected vs unexpected
- Expected: 404 not found, 400 invalid — handled explicitly. Unexpected: bugs → 500 without sensitive details.
Simple example: Salim returns 404 for an unknown product, generic 500 for a bug.
What it is not: a raw stack trace returned to the client in production.
Reflex: the client gets the right code, never our secrets.
- Automated test (Jest)
- Code verifying code: given input → expected output (expect().toBe()). Unit, integration, end to end.
Simple example: Salim tests his total function then his POST route.
What it is not: a single manual check in the browser.
Reflex: write the test proving it works.
- Non-regression
- Re-running tests after each change to detect what you broke unintentionally.
Simple example: Salim re-runs his suite before each release and sleeps well.
What it is not: discovering breakage in production via a client ticket.
Reflex: green suite required before each merge.
In practice — In practice — Hardening an API (validation, errors, tests)
- Validate a route's inputs with Zod or Joi: define a schema, reject the invalid with 400 and a clear message.
- Set up the centralised error middleware (err, req, res, next) last; wrap awaits in try/catch.
- Distinguish expected errors (404, 400) from unexpected ones (500); disclose no sensitive details in production.
- Write Jest tests: one unit (expect().toBe()) and one route test; re-run them after each change.
Ready-to-say script — announcing the hardening (30 s)
“This is Salim: before going live, each input is schema-validated with a clear 400 rejection, all errors go through a centralised middleware leaking no details, and the Jest suite covers both unit and routes — re-run on each change for non-regression.”
Say before each release; show the green suite.
Copy-paste template — Zod validation + errors + Jest test (tested code)
// 1) Validation (npm i zod) + 2) centralised errors
const express = require('express');
const { z } = require('zod');
const app = express();
app.use(express.json());
const schemaProduit = z.object({
nom: z.string().min(1, 'name required'),
prix: z.number().positive('positive price expected')
});
const produits = [];
app.post('/api/v1/produits', (req, res, next) => {
try {
const donnees = schemaProduit.parse(req.body); // throws if invalid
const produit = { id: produits.length + 1, ...donnees };
produits.push(produit);
res.status(201).json(produit);
} catch (err) {
err.statut = 400; // expected: invalid input
next(err);
}
});
// Error middleware: ALWAYS last
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
const statut = err.statut || 500;
const message = statut === 500 ? 'Internal error' : err.message;
res.status(statut).json({ erreur: message });
});
module.exports = app;
// 3) Jest test (app.test.js file, npm i -D jest supertest):
// const request = require('supertest');
// const app = require('./app');
// test('POST without name → 400', async () => {
// const res = await request(app).post('/api/v1/produits').send({ prix: 10 });
// expect(res.status).toBe(400);
// });
// test('Valid POST → 201', async () => {
// const res = await request(app).post('/api/v1/produits').send({ nom: 'Couscoussier', prix: 2500 });
// expect(res.status).toBe(201);
// expect(res.body.nom).toBe('Couscoussier');
// });
Learning prototype
Minimal validation + 2-3 tests on the critical path: instructive without slowing exploration.
Real production API
Like Salim: a schema on each external input, centralised middleware, Jest suite required green before merge.
TypeScript project
Zod validates AND types: one schema for runtime safety and compile-time robustness.
Team discovering tests
Start with critical routes (creation, login): immediate value, habit installed.
✓ Best practices
- Validate before processing, reject with a clear 400.
- Centralise errors in a single middleware.
- Test and re-run after each change.
✗ Common pitfalls
- Processing req.body without validation.
- Returning the stack trace to the client.
- Testing only by hand, once.
Key takeaways
- VALIDATION (Zod/Joi): check shape/types/constraints BEFORE processing, clear 400 if invalid. Never trust the client.
- Centralised ERRORS: middleware (err, req, res, next) last → consistent response; handle ASYNC (try/catch).
- JEST TESTS: unit + integration (route). expect(resultat).toBe(attendu).
- Benefits: CONFIDENCE, NON-REGRESSION, documentation, better design. The prototype → professional leap.
48h challenge — apply tomorrow
Add a validation schema to your most critical route, the error middleware, and 2 Jest tests (1 rejection 400, 1 success) — green suite required.
- I can explain validation, error middleware, Jest.
- I know how to write a clear 400 and a leak-free 500.
- I have 2 green tests on my critical route.
Frequently asked questions
Why test when “it already works”?
Because “it works” only holds for the hand-checked cases, today. Tomorrow, each change risks silently breaking something else (regression) — and without tests, you would have to re-check everything by hand each time, which never happens. Tests bring confidence (you know, you don't believe), non-regression (immediate alert after each change), documentation of expected behaviour, and push you to structure testable code better.
Validation, errors, tests: do you need all of them, for every project?
Dose according to stakes. Throwaway prototype: lighten up to move fast, but practise a minimum (good habits). Real application (users, production, team): essential — validation (corrupt data, bugs, flaws), errors (otherwise the server crashes or leaks details), tests (otherwise regressions reach production). These three practices turn a prototype into a reliable application.
Where to place the Express error-handling middleware?