ITSkillsCenter
Blog

Les principes REST en 5 minutes

16 دقائق للقراءة

Ce que vous saurez faire à la fin

  1. Concevoir une API REST conforme aux conventions modernes : ressources, verbes HTTP, codes statut, pagination et versioning.
  2. Implémenter une API REST en Node.js (Express) ou Python (FastAPI) avec authentification JWT et validation d’entrées.
  3. Documenter votre API avec OpenAPI 3.1 et Swagger UI pour qu’un développeur tiers puisse l’intégrer en 30 minutes.
  4. Mettre en place rate limiting, CORS, logs d’audit et gestion d’erreurs uniformes pour la production.
  5. Tester votre API avec Postman, écrire des tests automatiques et déployer derrière Nginx avec HTTPS gratuit.

Durée : 4h. Pré-requis : Node.js 20+ ou Python 3.11+ installé, terminal SSH, VPS Ubuntu (Hetzner CX11 à 3 000 FCFA/mois pour la prod), Postman gratuit, notions HTTP de base, budget total : 0 à 5 000 FCFA/mois.

Étape 1 — Les principes REST en 5 minutes

REST (Representational State Transfer) repose sur 6 contraintes : architecture client-serveur, stateless (pas de session sur le serveur), cacheable, interface uniforme, système en couches, code à la demande optionnel. En pratique, cela signifie : URLs nommées d’après les ressources (factures, clients), verbes HTTP standards (GET pour lire, POST pour créer, PUT/PATCH pour modifier, DELETE pour supprimer), codes statut HTTP standards (200 OK, 201 Created, 404 Not Found, 422 Unprocessable Entity).

Pour une PME africaine qui ouvre son SI à des partenaires (Wave, Orange Money, transporteur, comptable externe), une API REST bien conçue divise le coût d’intégration par 5. Pour une mauvaise API, prévoyez 200 000 FCFA d’aller-retour par intégrateur. Pour une bonne, 30 000 FCFA suffisent.

Étape 2 — Nommer correctement les ressources

BON :
GET    /api/v1/clients              -> lister tous les clients
GET    /api/v1/clients/42           -> détail du client 42
POST   /api/v1/clients              -> créer un client
PATCH  /api/v1/clients/42           -> modifier partiellement
PUT    /api/v1/clients/42           -> remplacer entièrement
DELETE /api/v1/clients/42           -> supprimer

GET    /api/v1/clients/42/factures  -> factures du client 42
POST   /api/v1/clients/42/factures  -> créer facture pour 42
GET    /api/v1/factures/100/pdf     -> PDF de la facture 100

MAUVAIS :
GET    /api/getClients              -> verbe dans l'URL
POST   /api/v1/createClient         -> redondant avec POST
GET    /api/v1/clients/delete/42    -> GET ne doit pas modifier
POST   /api/v1/client42removeNow    -> URL non normalisée

RÈGLES :
- Toujours au pluriel : clients pas client
- Hiérarchie max 3 niveaux : /clients/42/factures (oui)
  pas /clients/42/factures/100/lignes/5 (non, créer route dédiée)
- Pas de verbes dans l'URL (sauf actions spéciales : /factures/100/payer)
- Versioning dans l'URL : /api/v1/ pour pouvoir évoluer sans casser

Ces conventions sont universelles : Stripe, GitHub, Twitter, Mastodon les suivent toutes. Un développeur sénégalais qui a déjà intégré l’API Wave reconnaîtra immédiatement les patterns dans votre API.

Étape 3 — Codes HTTP : les 12 essentiels

SUCCÈS (2xx) :
200 OK              : requête réussie avec contenu
201 Created         : ressource créée (réponse au POST)
204 No Content      : succès sans contenu (DELETE typique)

REDIRECTION (3xx) :
301 Moved Permanently : URL déplacée définitivement
304 Not Modified     : cache valide (utiliser ETag)

ERREURS CLIENT (4xx) :
400 Bad Request     : JSON malformé, syntaxe incorrecte
401 Unauthorized    : pas authentifié (token manquant/invalide)
403 Forbidden       : authentifié mais pas le droit
404 Not Found       : ressource n'existe pas
409 Conflict        : conflit (ex: email déjà utilisé)
422 Unprocessable   : validation échouée (ex: prix négatif)
429 Too Many Requests : rate limit dépassé

ERREURS SERVEUR (5xx) :
500 Internal Server Error : exception non gérée
503 Service Unavailable   : maintenance ou surcharge

Différence cruciale 401 vs 403 : 401 = « qui êtes-vous ? », 403 = « je sais qui vous êtes mais vous n’avez pas le droit ». 422 vs 400 : 400 = JSON cassé, 422 = JSON valide mais business rule violée (prix négatif, email déjà pris).

Étape 4 — Initialiser un projet Express (Node.js)

# Créer le dossier
mkdir api-pme-dakar && cd api-pme-dakar
npm init -y

# Installer les dépendances
npm install express dotenv helmet cors express-rate-limit
npm install jsonwebtoken bcryptjs zod
npm install pg morgan compression
npm install --save-dev nodemon

# Structure de fichiers
mkdir src
touch src/server.js src/db.js src/routes.js
touch .env .gitignore

# .gitignore
cat > .gitignore << 'EOF'
node_modules
.env
logs/
*.log
EOF

# .env (ne JAMAIS commiter)
cat > .env << 'EOF'
PORT=3000
NODE_ENV=development
JWT_SECRET=changez_moi_par_64_caracteres_aleatoires_2026_dakar_senegal
DATABASE_URL=postgresql://app_pme:password@localhost:5432/pme_dakar
EOF

# Script package.json
npm pkg set scripts.dev="nodemon src/server.js"
npm pkg set scripts.start="node src/server.js"

Toujours séparer config (variables environnement) et code. Ne jamais hardcoder un mot de passe. Utilisez un secret JWT d’au moins 64 caractères aléatoires (générer avec openssl rand -hex 32).

Étape 5 — Squelette d’API Express avec middlewares

// src/server.js
require('dotenv').config()
const express = require('express')
const helmet = require('helmet')
const cors = require('cors')
const morgan = require('morgan')
const compression = require('compression')
const rateLimit = require('express-rate-limit')

const app = express()

// Middlewares de sécurité
app.use(helmet())  // headers de sécurité par défaut
app.use(cors({
    origin: ['https://pme-dakar.sn', 'https://app.pme-dakar.sn'],
    credentials: true
}))
app.use(express.json({limit: '1mb'}))
app.use(compression())
app.use(morgan('combined'))

// Rate limiting global : 100 requêtes / 15 min / IP
app.use('/api/', rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100,
    message: {error: 'Trop de requêtes, réessayez dans 15 minutes'}
}))

// Routes
app.use('/api/v1', require('./routes'))

// Healthcheck
app.get('/health', (req, res) => res.json({
    status: 'ok',
    timestamp: new Date().toISOString()
}))

// 404 par défaut
app.use((req, res) => res.status(404).json({
    error: 'Route non trouvée',
    path: req.path
}))

// Gestion d'erreurs centralisée
app.use((err, req, res, next) => {
    console.error(err.stack)
    res.status(err.status || 500).json({
        error: err.message || 'Erreur interne'
    })
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => console.log(`API en écoute sur :${PORT}`))

Ordre des middlewares important : helmet et cors AVANT json (sécurité d’abord), rate limit AVANT les routes. La gestion d’erreurs en dernier capture toutes les exceptions non traitées.

Étape 6 — Routes CRUD pour la ressource clients

// src/routes.js
const express = require('express')
const router = express.Router()
const {z} = require('zod')
const db = require('./db')
const {authenticate} = require('./auth')

// Schéma de validation Zod
const ClientSchema = z.object({
    nom: z.string().min(2).max(150),
    telephone: z.string().regex(/^\+221[0-9]{9}$/),
    email: z.string().email().optional(),
    ville: z.string().default('Dakar')
})

// GET /api/v1/clients - lister avec pagination
router.get('/clients', authenticate, async (req, res, next) => {
    try {
        const page = parseInt(req.query.page) || 1
        const limit = Math.min(parseInt(req.query.limit) || 20, 100)
        const offset = (page - 1) * limit

        const result = await db.query(
            'SELECT id, nom, telephone, email, ville FROM clients ORDER BY nom LIMIT $1 OFFSET $2',
            [limit, offset]
        )
        const count = await db.query('SELECT COUNT(*) FROM clients')

        res.json({
            data: result.rows,
            pagination: {
                page, limit,
                total: parseInt(count.rows[0].count),
                pages: Math.ceil(count.rows[0].count / limit)
            }
        })
    } catch (e) { next(e) }
})

// GET /api/v1/clients/:id
router.get('/clients/:id', authenticate, async (req, res, next) => {
    try {
        const result = await db.query(
            'SELECT * FROM clients WHERE id = $1', [req.params.id])
        if (result.rows.length === 0)
            return res.status(404).json({error: 'Client non trouvé'})
        res.json(result.rows[0])
    } catch (e) { next(e) }
})

// POST /api/v1/clients
router.post('/clients', authenticate, async (req, res, next) => {
    try {
        const data = ClientSchema.parse(req.body)
        const result = await db.query(
            `INSERT INTO clients (nom, telephone, email, ville)
             VALUES ($1, $2, $3, $4)
             RETURNING id, nom, telephone, email, ville`,
            [data.nom, data.telephone, data.email, data.ville])
        res.status(201)
            .location(`/api/v1/clients/${result.rows[0].id}`)
            .json(result.rows[0])
    } catch (e) {
        if (e.name === 'ZodError')
            return res.status(422).json({error: 'Validation', details: e.errors})
        if (e.code === '23505')
            return res.status(409).json({error: 'Email ou téléphone déjà utilisé'})
        next(e)
    }
})

module.exports = router

Validation Zod transforme automatiquement et garantit que les données entrantes correspondent au schéma. La réponse 201 Created inclut le header Location pointant vers la nouvelle ressource (convention REST).

Étape 7 — Authentification JWT

// src/auth.js
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const db = require('./db')

const SECRET = process.env.JWT_SECRET

// POST /api/v1/auth/login
async function login(req, res) {
    const {email, password} = req.body
    const result = await db.query(
        'SELECT id, email, password_hash, role FROM users WHERE email = $1',
        [email])

    if (result.rows.length === 0)
        return res.status(401).json({error: 'Identifiants invalides'})

    const user = result.rows[0]
    const ok = await bcrypt.compare(password, user.password_hash)
    if (!ok)
        return res.status(401).json({error: 'Identifiants invalides'})

    const token = jwt.sign(
        {sub: user.id, email: user.email, role: user.role},
        SECRET,
        {expiresIn: '24h', issuer: 'api-pme-dakar'}
    )

    res.json({token, expires_in: 86400, token_type: 'Bearer'})
}

// Middleware d'authentification
function authenticate(req, res, next) {
    const auth = req.headers.authorization
    if (!auth || !auth.startsWith('Bearer '))
        return res.status(401).json({error: 'Token manquant'})

    const token = auth.split(' ')[1]
    try {
        req.user = jwt.verify(token, SECRET)
        next()
    } catch (e) {
        return res.status(401).json({error: 'Token invalide ou expiré'})
    }
}

// Middleware de contrôle de rôle
function requireRole(role) {
    return (req, res, next) => {
        if (req.user.role !== role && req.user.role !== 'admin')
            return res.status(403).json({error: 'Permission refusée'})
        next()
    }
}

module.exports = {login, authenticate, requireRole}

JWT (JSON Web Token) est stateless : le serveur ne stocke aucune session. Avantage : scalable, simple. Inconvénient : impossible de révoquer un token avant expiration sans blacklist Redis. Pour les opérations critiques, durée 1h max.

Étape 8 — Pagination, filtrage et tri

Convention de query string :
GET /api/v1/factures?page=2&limit=50&sort=-date_emission&statut=payee&client_id=42

- page : numéro de page (défaut 1)
- limit : éléments par page (défaut 20, max 100)
- sort : champ de tri, "-" devant pour décroissant
- filtres : nom du champ = valeur

Réponse standardisée :
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 50,
    "total": 1234,
    "pages": 25
  },
  "links": {
    "first": "/api/v1/factures?page=1&limit=50",
    "prev":  "/api/v1/factures?page=1&limit=50",
    "next":  "/api/v1/factures?page=3&limit=50",
    "last":  "/api/v1/factures?page=25&limit=50"
  }
}

Pour grandes collections (millions de lignes),
préférer la pagination par curseur :
GET /api/v1/logs?cursor=eyJpZCI6MTIzNDV9&limit=50

Pagination par offset (page=N) ralentit linéairement avec la profondeur. Au-delà de 10 000 résultats, passez à la pagination par curseur (basée sur l’ID du dernier élément).

Étape 9 — Documenter avec OpenAPI 3.1 et Swagger UI

# Installer les outils
npm install swagger-jsdoc swagger-ui-express
// src/swagger.js
const swaggerJsdoc = require('swagger-jsdoc')
const swaggerUi = require('swagger-ui-express')

const options = {
    definition: {
        openapi: '3.1.0',
        info: {
            title: 'API PME Dakar',
            version: '1.0.0',
            description: 'API REST pour gestion clients et factures',
            contact: {email: 'dev@pme-dakar.sn'}
        },
        servers: [
            {url: 'https://api.pme-dakar.sn/v1', description: 'Production'},
            {url: 'http://localhost:3000/api/v1', description: 'Dev'}
        ],
        components: {
            securitySchemes: {
                bearerAuth: {type: 'http', scheme: 'bearer', bearerFormat: 'JWT'}
            }
        },
        security: [{bearerAuth: []}]
    },
    apis: ['./src/routes/*.js']
}

const spec = swaggerJsdoc(options)

module.exports = (app) => {
    app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec))
    app.get('/openapi.json', (req, res) => res.json(spec))
}

// Annoter les routes avec JSDoc
/**
 * @openapi
 * /clients:
 *   get:
 *     summary: Lister les clients
 *     tags: [Clients]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema: {type: integer, default: 1}
 *     responses:
 *       200:
 *         description: Liste paginée
 */

Swagger UI génère automatiquement une page web interactive sur /docs où n’importe quel développeur peut tester votre API en direct, voir les schémas, copier les exemples curl. Indispensable pour les intégrations partenaires.

Étape 10 — Versioning : stratégies et bonnes pratiques

3 stratégies courantes :

1. URL versioning (recommandé pour PME) :
   /api/v1/clients
   /api/v2/clients
   - Simple, visible, cacheable
   - Permet de faire cohabiter v1 et v2 plusieurs mois

2. Header versioning :
   GET /api/clients
   Accept: application/vnd.pme.v2+json
   - Plus "pur" REST
   - Plus complexe à tester depuis le navigateur

3. Query parameter :
   /api/clients?version=2
   - À éviter : pollue les caches

RÈGLES POUR ÉVITER LES BREAKING CHANGES :
- Ajouter un champ : OK, pas besoin de v2
- Supprimer un champ : créer v2
- Changer le type d'un champ : créer v2
- Renommer un champ : créer v2 ou supporter les deux noms

DEPRECATION :
Annoncer 6 mois à l'avance via header :
Deprecation: true
Sunset: Fri, 31 Dec 2026 23:59:59 GMT
Link: <https://api.pme-dakar.sn/v2/clients>; rel="successor-version"

Une bonne API ne casse jamais ses utilisateurs sans préavis. Stripe maintient son API v1 depuis 2011. Pour une PME, garder v1 fonctionnelle 12 mois après le lancement de v2 est un minimum éthique.

Étape 11 — Idempotence et clé d’idempotence

Verbes idempotents (le même appel répété donne le même résultat) :
GET, PUT, DELETE, HEAD : OUI
POST : NON par défaut (chaque appel crée une nouvelle ressource)

Problème : un mobile en zone faible (Pikine, Kaolack) envoie POST /factures,
le réseau se coupe avant la réponse, le client retente -> 2 factures créées.

Solution : Idempotency-Key (utilisé par Stripe, Adyen)

POST /api/v1/factures
Idempotency-Key: 9f8b2e1a-uuid-unique-cote-client
Content-Type: application/json
{...}

Côté serveur (pseudo-code) :
1. Récupérer Idempotency-Key du header
2. Chercher dans Redis : key="idem:<key>"
3. Si trouvée : retourner la réponse cachée (200/201 + body)
4. Sinon : exécuter la requête, mettre la réponse en cache 24h
async function idempotencyMiddleware(req, res, next) {
    const key = req.headers['idempotency-key']
    if (!key || req.method !== 'POST') return next()

    const cached = await redis.get(`idem:${key}`)
    if (cached) {
        const data = JSON.parse(cached)
        return res.status(data.status).json(data.body)
    }

    const originalJson = res.json.bind(res)
    res.json = (body) => {
        redis.setex(`idem:${key}`, 86400,
            JSON.stringify({status: res.statusCode, body}))
        return originalJson(body)
    }
    next()
}

Idempotency-Key sauve la mise pour les paiements et opérations financières. Une PME qui intègre Wave ou Orange Money DOIT implémenter ce pattern, sinon ses utilisateurs paieront deux fois en zone réseau instable.

Étape 12 — Gestion d’erreurs uniforme (Problem Details RFC 7807)

// Format standardisé des réponses d'erreur
function errorResponse(res, status, type, title, detail, errors) {
    return res.status(status)
        .type('application/problem+json')
        .json({
            type: `https://api.pme-dakar.sn/errors/${type}`,
            title,
            status,
            detail,
            instance: res.req.originalUrl,
            errors: errors || undefined,
            timestamp: new Date().toISOString()
        })
}

// Exemples d'utilisation
errorResponse(res, 422, 'validation-failed',
    'Validation des données échouée',
    'Le champ téléphone est invalide',
    [{field: 'telephone', message: 'Format +221XXXXXXXXX requis'}])

errorResponse(res, 404, 'resource-not-found',
    'Ressource non trouvée',
    'Aucun client avec id=42')

errorResponse(res, 429, 'rate-limit-exceeded',
    'Limite de requêtes atteinte',
    'Maximum 100 requêtes par 15 minutes par IP')

RFC 7807 standardise le format des erreurs HTTP. Un client peut parser n’importe quelle erreur de votre API avec le même code. Réduit le temps d’intégration de 30%.

Étape 13 — Tester avec Postman et tests automatiques

# Postman gratuit : postman.com/downloads
# Importer la spec OpenAPI directement :
#   File -> Import -> Link -> https://api.pme-dakar.sn/openapi.json

# Test automatique avec Jest (Node.js)
npm install --save-dev jest supertest

# tests/clients.test.js
const request = require('supertest')
const app = require('../src/server')

describe('GET /api/v1/clients', () => {
    let token

    beforeAll(async () => {
        const res = await request(app)
            .post('/api/v1/auth/login')
            .send({email: 'admin@test.sn', password: 'test123'})
        token = res.body.token
    })

    it('retourne 401 sans token', async () => {
        const res = await request(app).get('/api/v1/clients')
        expect(res.status).toBe(401)
    })

    it('retourne la liste avec token valide', async () => {
        const res = await request(app)
            .get('/api/v1/clients')
            .set('Authorization', `Bearer ${token}`)
        expect(res.status).toBe(200)
        expect(res.body.data).toBeInstanceOf(Array)
        expect(res.body.pagination).toBeDefined()
    })

    it('crée un nouveau client', async () => {
        const res = await request(app)
            .post('/api/v1/clients')
            .set('Authorization', `Bearer ${token}`)
            .send({nom: 'Test', telephone: '+221770000000'})
        expect(res.status).toBe(201)
        expect(res.body.id).toBeDefined()
    })
})

# Lancer
npm test

Postman Collections permettent de partager des suites de tests avec votre équipe. Newman (CLI Postman) intègre les tests dans CI/CD. Couverture cible : tous les endpoints critiques avec 1 test happy path et 2 tests d’erreur.

Étape 14 — Déployer en production avec Nginx et HTTPS

# Sur le VPS Ubuntu
sudo apt install -y nginx certbot python3-certbot-nginx

# Lancer l'API avec PM2 (process manager)
sudo npm install -g pm2
cd /var/www/api-pme-dakar
pm2 start src/server.js --name api-pme
pm2 startup && pm2 save

# Configurer Nginx en reverse proxy
sudo nano /etc/nginx/sites-available/api.pme-dakar.sn

server {
    listen 80;
    server_name api.pme-dakar.sn;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Compression
    gzip on;
    gzip_types application/json text/plain;

    # Limite taille du body
    client_max_body_size 10m;
}

# Activer le site
sudo ln -s /etc/nginx/sites-available/api.pme-dakar.sn /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# Certificat HTTPS gratuit Let's Encrypt
sudo certbot --nginx -d api.pme-dakar.sn

# Vérifier
curl -I https://api.pme-dakar.sn/health

Nginx en reverse proxy gère HTTPS, gzip, headers de sécurité, et permet de redémarrer Node.js sans coupure. Let’s Encrypt renouvelle le certificat automatiquement tous les 90 jours, gratuitement.

Erreurs fréquentes

  • Mettre des verbes dans les URLs : /api/getClients, /api/createUser. Utilisez les verbes HTTP (GET, POST), pas les URLs.
  • Retourner 200 OK avec un message d’erreur dans le body : utilisez les codes HTTP appropriés (4xx, 5xx). Sinon les outils et caches s’embrouillent.
  • Stocker des secrets dans le code source : variables d’environnement obligatoires. Un secret poussé sur GitHub est compromis en moins de 5 minutes.
  • Pas de rate limiting : un script malveillant peut faire 10 000 req/s et faire tomber votre serveur. Toujours limiter par IP et par utilisateur.
  • Pas de validation d’entrée : injection SQL, XSS, montants négatifs. Validez TOUTE entrée utilisateur avec Zod, Joi, ou Pydantic.
  • Pas de versioning : impossible d’évoluer sans casser les clients existants. Mettez /v1/ dès le premier jour.
  • JWT avec expiration trop longue : 30 jours = un token volé reste valide 30 jours. 1h pour les opérations sensibles, 24h max pour le reste.

Checklist de mise en production

  • Conventions REST respectées : ressources au pluriel, verbes HTTP standards
  • Codes HTTP appropriés (200, 201, 401, 403, 404, 422, 500)
  • Versioning dans l’URL (/api/v1/)
  • Authentification JWT avec secret de 64+ caractères en env var
  • Validation des entrées avec Zod, Joi ou Pydantic
  • Gestion d’erreurs uniforme (RFC 7807)
  • Pagination, filtrage, tri standardisés sur les collections
  • Rate limiting global + par utilisateur authentifié
  • CORS configuré avec liste blanche d’origines
  • Headers de sécurité via Helmet
  • Documentation OpenAPI 3.1 + Swagger UI sur /docs
  • Tests automatiques pour endpoints critiques (Jest + Supertest)
  • HTTPS Let’s Encrypt + renouvellement auto via certbot
  • PM2 ou systemd pour redémarrage automatique en cas de crash
Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité