4.4 · Validation, error handling & tests (Jest)

Level 4 · API, authentication & real time

4.4Validation, error handling & tests (Jest)

Goal: Harden an API: input validation (Zod/Joi), clean centralised error handling, automated tests (Jest).
Estimated time: 12 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “Testing is wasting time: my code works, I checked it by hand.” No. Manual checking only covers a few cases, today only — and each future change risks silently breaking what worked (regression). Tests re-run in seconds after each change and alert at once: this non-regression is what lets you change and refactor without fear. “It works” without tests means “it works for now”.
A hardened request's path Validate → process → handle → test Validation Zod/Joischema, 400 if invalidProcessing Logic +try/catch on awaitErrors Centralisedmiddleware lastTests Jest: unit +route, re-run
Validate → process → handle → test
Text version of the diagram (SEO / screen readers / translation): 1) Salim validates each request body with a schema: invalid → clear 400, before any processing. 2) He wraps his awaits in try/catch and directs errors to a centralised middleware placed last. 3) This middleware answers cleanly: 404/400 for the expected, 500 without sensitive details for the unexpected. 4) He covers it all with Jest tests re-run on each change. Lesson: validate, centralise, test.

🔎 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)

  1. Validate a route's inputs with Zod or Joi: define a schema, reject the invalid with 400 and a clear message.
  2. Set up the centralised error middleware (err, req, res, next) last; wrap awaits in try/catch.
  3. Distinguish expected errors (404, 400) from unexpected ones (500); disclose no sensitive details in production.
  4. Write Jest tests: one unit (expect().toBe()) and one route test; re-run them after each change.
You harden your API: validation (Zod/Joi), clean centralised errors, Jest tests — what separates a prototype from a professional application.

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.

Caution: Reliability is your credibility: validate every external input and never return sensitive technical details (traces, queries, secrets) in error responses in production — generic message on 500. This guide's examples use fictitious data only. Teaching content, not a quality audit.

✓ 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.

Section exit checklist
  • 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.
Self-rating 1-5: is each external input validated?does my suite pass after my last change?

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.

Check your understanding

Where to place the Express error-handling middleware?

Additional resources (optional, to go further)

Level 4 examFresh, harder questions to test what you've learned