Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

Concepts communs (agnostiques au langage)

Confirmé 35 min prérequis : axes 5-7 lus

🎯 Objectif : maîtriser le vocabulaire et les patterns que tu retrouveras à l’identique dans Node, Python ou PHP. Ces 6 thèmes couvrent 80 % de ce qu’on fait sur un backend web.

À l'issue de cet axe, tu sauras :

  • Décrire le pipeline d'une requête : middlewares → handler → réponse
  • Concevoir une API REST conforme (méthodes, codes, idempotence, pagination)
  • Valider toutes les entrées avec un schéma (Zod, Pydantic, Symfony Validator)
  • Choisir entre sessions, JWT, Passkeys ; entre RBAC et ABAC
  • Faire tourner un job de fond avec une file (Redis/BullMQ, Celery, Symfony Messenger)
  • Configurer logs structurés et variables d'environnement façon 12-factor

Avant tout : tu as déjà couvert HTTP en profondeur dans l’axe 2. Ici, on regarde ce que le serveur fait quand une requête arrive.

flowchart LR
    Req[Requête HTTP] --> M1[Logger]
    M1 --> M2[CORS]
    M2 --> M3[Body parser]
    M3 --> M4[Auth]
    M4 --> M5[Validation]
    M5 --> H[Handler<br/>logique métier]
    H --> Resp[Réponse HTTP]
    H -.erreur.-> EH[Error handler]
    EH --> Resp
Pipeline d'une requête HTTP côté serveur

Un middleware est une fonction qui intercepte la requête, fait quelque chose, puis passe la main à la suivante (ou court-circuite). C’est le pattern universel — qu’on l’appelle middleware (Express, Fastify, Koa), interceptor (NestJS), middleware (Django/FastAPI/Laravel), ou guard (NestJS, Symfony).

Exemple en Node/Hono :

import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
const app = new Hono();
app.use('*', logger());
app.use('*', cors());
app.use('/admin/*', async (c, next) => {
if (!c.req.header('X-Admin-Key')) return c.json({ error: 'Unauthorized' }, 401);
await next();
});

L’ordre compte : logger en premier (sinon il rate les erreurs), auth avant validation (sinon on valide pour rien), gestion d’erreur en dernier.

  • Logger structuré (JSON) : permet le grep efficace en prod.
  • CORS restrictif : pas * en prod sauf API publique.
  • Rate-limit sur les endpoints sensibles (login, signup, reset password).
  • Body size limit : un POST de 100 Mo non attendu = DoS.
  • Helmet ou équivalent : ajoute en-têtes de sécu (X-Frame-Options, etc.).

Pour les 80 % des cas, REST suffit. Les principes essentiels :

PrincipeExemple
Ressource identifiée par URL/tasks/42, pas /getTask?id=42
Méthode HTTP par actionGET, POST, PUT, PATCH, DELETE
Codes de statut standards201 à la création, 204 pour DELETE, 404, 422, 429
IdempotencePUT /tasks/42 10 fois = 1 fois
Sans étatPas de session serveur dépendante de la requête précédente
GET /tasks?status=done&sort=-created_at&page=2&limit=20

3 styles courants :

StyleExempleQuand utiliser
Offset?offset=40&limit=20Petits volumes, tri stable
Cursor?after=abc123&limit=20Gros volumes, pagination temps réel
Page?page=3&limit=20UX simple, frontends classiques

Réponse paginée standard :

{
"data": [...],
"pagination": {
"total": 142,
"page": 3,
"limit": 20,
"next_cursor": "abc123"
}
}

3 façons de versionner une API :

MéthodeExempleVerdict
URL/v1/tasksLe plus lisible, le plus utilisé
HeaderAccept: application/vnd.api.v1+jsonPlus propre, plus strict
Query?version=1À éviter, mal conventionnel

Conseil pratique : versionne dès le 1er release public (/v1). Ajouter une version après coup est pénible.

Spécification standard pour décrire ton API. Outils : génération de doc (Swagger UI, Redoc), génération de clients (TypeScript, Python, Go), validation de contrat.

# openapi.yaml — extrait
paths:
/tasks/{id}:
get:
parameters:
- in: path
name: id
schema: { type: integer }
responses:
'200':
description: La tâche
content:
application/json:
schema: { $ref: '#/components/schemas/Task' }
'404': { description: Introuvable }

FastAPI la génère gratuitement, NestJS via décorateurs, Symfony API Platform également. C’est le gold standard.

Quand préférer
GraphQLFrontends qui agrègent beaucoup de données ; mobile (1 requête au lieu de 10)
gRPCMicroservices internes, performance critique, stream bidirectionnel
tRPCStack 100 % TypeScript end-to-end ; pas d’API publique exposée

REST reste le défaut universel en 2026. Les autres ont leur niche.

Règle absolue : valider TOUS les inputs côté serveur. Le client est hostile par défaut.

Au lieu de if (!body.email || !body.email.includes('@')), déclare un schéma qui valide ET type :

LangageLib
TypeScriptZod (le plus utilisé), Valibot, ArkType
PythonPydantic (cœur de FastAPI)
PHPSymfony Validator, Laravel Validation
Java/KotlinBean Validation (@NotNull, @Email)
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(2000).optional(),
due_at: z.string().datetime().optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
type CreateTaskInput = z.infer<typeof CreateTaskSchema>;
// Dans le handler
const body = CreateTaskSchema.parse(await req.json());
// ^ throw ZodError si invalide
// body est typé CreateTaskInput

Quand tu retournes une ressource, tu choisis ce que tu exposes :

// ❌ Tu exposes le password hash, l'IP de connexion, le token de reset…
res.json(await db.user.findById(id));
// ✅ Tu exposes uniquement ce qui doit l'être
const user = await db.user.findById(id);
res.json({
id: user.id,
name: user.name,
email: user.email,
// password, ip, reset_token NON exposés
});

Beaucoup d’ORM proposent des mécanismes : select Prisma, serializers DRF, view objects Symfony. Utilise-les systématiquement.

Authentification = qui es-tu ? Autorisation = qu’as-tu le droit de faire ?

Deux questions distinctes, à ne pas confondre.

StratégieComment ça marcheQuand l’utiliser
Session + cookieLe serveur stocke la session, le client renvoie un cookieApps web monolithes (Rails, Django, Laravel)
JWT (Bearer)Token signé contenant les claims, vérifié par le serveurAPI stateless, mobile, microservices
JWT en cookie HttpOnlyLe meilleur des deuxSPA + API (recommandé)
OAuth 2.1 + OIDCDélégué à un fournisseur (Google, GitHub)« Sign in with X »
Magic linksEmail → lien à usage uniquePasswordless, UX simple
Passkeys (WebAuthn)Clé publique stockée côté serveur, signature côté deviceStandard 2026+ — sécurité maximale, UX excellente
1. Login : POST /auth/login { email, password }
2. Serveur vérifie, génère un JWT, pose un cookie HttpOnly Secure SameSite=Lax
3. Requêtes suivantes : le navigateur envoie le cookie auto, le serveur valide la signature
4. Logout : suppression du cookie

✅ Inaccessible au JS (donc pas volable par XSS). ✅ Auto-envoyé sur toutes les requêtes. ✅ SameSite=Lax protège du CSRF.

Jamais en clair. Jamais SHA-256 simple. Toujours un algo lent et salé conçu pour ça :

AlgoAvis
argon2id✅ Recommandé en 2026
bcrypt✅ Toujours valide
scrypt✅ Acceptable
MD5, SHA-1, SHA-256❌ Cassés ou trop rapides
import { hash, verify } from '@node-rs/argon2';
const hashed = await hash(plainPassword);
// ...
const ok = await verify(hashed, plainPassword);
ModèleSensExemple
RBAC (Role-Based)Rôles fixes : admin, editor, viewer« Les éditeurs peuvent créer un article »
ABAC (Attribute-Based)Règles sur attributs de l’utilisateur ET de la ressource« Un user peut éditer son propre profil mais pas celui des autres »
Policy-as-CodeDSL ou langage dédiéOPA (Rego), Cerbos

En pratique : commence en RBAC. Bascule vers ABAC quand des règles deviennent contextuelles. Policy-as-code pour gros systèmes.

// Exemple ABAC simple
function canEditTask(user: User, task: Task): boolean {
if (user.role === 'admin') return true;
if (task.ownerId === user.id) return true;
if (task.team.members.includes(user.id) && user.role === 'editor') return true;
return false;
}

Si une action prend > 1 seconde (envoi d’email, génération PDF, ré-encodage vidéo), ne la fais PAS dans le handler. Tu :

  1. Pousses la tâche dans une file (queue).
  2. Réponds immédiatement au client.
  3. Un worker consomme la file en arrière-plan.
flowchart LR
    C((Client)) --> API[API HTTP]
    API -->|push job| Q[(Queue<br/>Redis/RabbitMQ)]
    API -->|reponse 202<br/>immediate| C
    Q --> W1[Worker 1]
    Q --> W2[Worker 2]
    W1 --> DB[(DB)]
    W2 --> Email[Service email]
Architecture worker — la file découple producteur et consommateur
BrokerStyleCas typique
Redis + lib applicativeSimple, in-memory, persistantBullMQ (Node), Celery (Python), Sidekiq (Ruby)
RabbitMQAMQP, riche en routageApps complexes avec topics
KafkaPub/sub haut volume, durableEvent streaming, microservices à l’échelle
AWS SQS / GCP Pub-SubCloud managéAWS-native, peu d’ops
  • Idempotence du job : un même job réexécuté ne doit pas créer de doublons. Utilise un idempotency_key.
  • Retry exponentiel : 1 s, 5 s, 30 s, 2 min… Plafonne le nombre de retries (5 souvent).
  • Dead Letter Queue (DLQ) : les jobs qui échouent N fois vont dans une queue séparée pour analyse manuelle.
  • Cron / scheduled tasks : pour des jobs récurrents (rapport quotidien, cleanup hebdo). Soit cron OS, soit scheduler applicatif (BullMQ Cron, Celery Beat).
import { Queue, Worker } from 'bullmq';
const emailQueue = new Queue('emails', { connection: { host: 'localhost', port: 6379 } });
// Producteur (dans le handler)
await emailQueue.add('welcome', { userId: 42 }, {
attempts: 5,
backoff: { type: 'exponential', delay: 1000 },
});
// Worker (process séparé)
new Worker('emails', async (job) => {
await sendEmail(job.data.userId);
}, { connection: { host: 'localhost', port: 6379 } });

12factor.net — une référence essentielle. Les points clés pour un backend :

FacteurAction concrète
Config dans l’environnementprocess.env.DATABASE_URL, jamais en dur
Logs en flux d’événementsconsole.log → stdout, pas de fichier
StatelessPas d’état dans le process
DisposableDémarrage rapide, arrêt propre (SIGTERM gracieusement géré)
Dev/prod parityMêmes outils en dev qu’en prod (Docker, Postgres)
// ❌ Texte plat, dur à parser
console.log('User 42 logged in from 1.2.3.4');
// ✅ JSON structuré
logger.info({ event: 'user_login', userId: 42, ip: '1.2.3.4' });

Avec un agrégateur (Datadog, Loki, Sentry), tu peux :

  • Filtrer par champ (userId: 42).
  • Détecter des patterns.
  • Corréler les événements (par requestId injecté en début de pipeline).

Libs recommandées :

LangageLib
Nodepino, winston
Pythonstructlog, logging stdlib
PHPMonolog (standard Symfony/Laravel)
// ❌ try/catch dans chaque handler
app.get('/users/:id', async (req, res) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// ✅ Middleware d'erreur global
app.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id); // throw si problème
res.json(user);
});
app.use((err, req, res, next) => {
if (err instanceof NotFoundError) return res.status(404).json({ error: err.message });
if (err instanceof ZodError) return res.status(422).json({ errors: err.issues });
logger.error({ err, path: req.path });
res.status(500).json({ error: 'Server error' });
});

Couplé à Sentry ou équivalent : tu vois en quasi-temps réel chaque exception en prod, avec stacktrace, breadcrumbs, contexte utilisateur.

Tu reçois un POST /users avec un body JSON. Que fais-tu en premier ?
Tu envoies un email de confirmation à l'inscription. Le service email met 800 ms. Comment structurer ?
Tu hashes un mot de passe avec SHA-256 + sel. Avis ?

Piège réel rencontré — hono/jwt verify échoue : « JwtAlgorithmRequired » Auth

🩹 Symptôme

JwtAlgorithmRequired: JWT verification requires "alg" option to be specified
at verifyToken (src/lib/jwt.ts:22)

🔍 Cause

Depuis Hono 4.x, verify(token, secret) ne prend plus d’algorithme par défaut — il faut le passer explicitement. C’est une mesure de sécurité : ne pas spécifier l’algorithme rend l’app vulnérable à l’attaque dite alg-confusion (un attaquant qui forge un token avec alg=none, ou en HS256 contre une clé RS256 publique).

🩺 Fix

Passer explicitement l’algorithme à sign ET verify :

const ALG = 'HS256' as const;
export async function signToken(userId: number) {
return sign({ sub: String(userId), exp: ... }, JWT_SECRET, ALG);
}
export async function verifyToken(token: string) {
return verify(token, JWT_SECRET, ALG);
}

🧠 Leçon

Pour les libs sécurité, toujours lire le changelog avant d’upgrader. Quand une lib bouge ses defaults dans le sens « plus strict », c’est généralement parce qu’on a corrigé une vulnérabilité connue.

Piège réel rencontré — @hono/zod-validator renvoie 400 au lieu de 422 Tests

🩹 Symptôme

expect(res.status).toBe(422);
// AssertionError: expected 400 to be 422

🔍 Cause

Le zValidator de Hono renvoie un 400 Bad Request par défaut quand la validation échoue. Mais la convention REST moderne dit que 422 Unprocessable Entity est plus précis quand la requête est syntaxiquement valide mais sémantiquement invalide (un email mal formé, un champ requis absent…).

🩺 Fix

Wrapper zValidator dans un helper qui force 422 :

src/lib/validator.ts
import { zValidator } from '@hono/zod-validator';
import type { ZodSchema } from 'zod';
export function validate<T extends ZodSchema>(
target: 'json' | 'query' | 'param',
schema: T
) {
return zValidator(target, schema, (result, c) => {
if (!result.success) {
return c.json(
{ error: 'Validation failed', issues: result.error.issues },
422
);
}
});
}

Puis dans les routes : validate('json', MySchema) au lieu de zValidator(...).

🧠 Leçon

Quand un framework expose des defaults qui ne matchent pas ta convention (codes HTTP, format d’erreur, schéma de réponse), wrap-le dans un helper plutôt que de l’utiliser brut partout. Tu écris la convention une fois, et tu peux la changer une fois au lieu de 30 fois.

🔍 Tous les pièges du guide sont sur la page /pieges/ — searchable par symptôme.


Suite : choisis ton parcours :

  • Node.js / TypeScript — le plus pratique en full-stack JS, avec Hono comme défaut moderne
  • Python — Django pour CRUD complets, Flask pour minimaliste, FastAPI pour API async typée
  • PHP — PHP 8.4 + Laravel ou Symfony