Questions d'entretien REST API design
Les entretiens sur la conception d'API REST évaluent votre capacité à construire des API web évolutives, maintenables et conviviales. Ces entretiens sont courants pour les rôles backend, full-stack et ingénierie de plateforme, surtout aux niveaux seniors. Les intervieweurs évaluent votre compréhension des principes REST, de la modélisation des ressources, de la gestion des erreurs, de la pagination, du versioning et de la sécurité. Vous ferez face à la fois à des discussions conceptuelles et à des défis de codage pratiques.
Ce que couvrent les entretiens REST API design
Principes et contraintes RESTful
Comprendre l'apatridie, l'interface uniforme, l'identification des ressources et comment utiliser correctement les méthodes HTTP (GET, POST, PUT, DELETE, PATCH) et les codes d'état.
Modélisation et nommage des ressources
Concevoir des motifs URI intuitifs pour les ressources imbriquées, les noms au pluriel, le filtrage, le tri et éviter les verbes dans les points de terminaison.
Pagination, filtrage et versioning
Implémentation de la pagination basée sur curseur vs décalage, stratégies de filtrage/tri, et approches de versioning par URL vs en-tête.
Gestion des erreurs et sécurité
Concevoir des structures de réponse d'erreur cohérentes, l'utilisation appropriée des codes d'état HTTP, et les pratiques de sécurité courantes comme l'authentification, la limitation de débit et la validation des entrées.
Exemples de questions d'entretien REST API design
- Concevez une API REST pour un système d'articles de presse. Comment modéliseriez-vous les ressources et les points de terminaison pour les articles, les commentaires et les balises ?Ce qu'une bonne réponse couvre
- Identifier les ressources principales : articles, commentaires, balises, auteurs.
- Utiliser des URIs hiérarchiques : /articles/{id}/commentaires, /articles/{id}/balises.
- Prévoir des endpoints pour les relations many-to-many (articles/balises) via sous-ressources ou une ressource dédiée.
- Considérer l'inclusion de ressources liées via des query params (embed, expand) pour éviter le N+1.
- Tenir compte de l'authentification, pagination, et filtres pour des réponses volumineuses.
Voir un exemple de réponse
Une API REST pour un système d'articles représente chaque entité comme une ressource. Articles : /articles (GET pour lister, POST pour créer), /articles/{id} (GET, PUT, PATCH, DELETE). Commentaires : /articles/{id}/commentaires (GET, POST), /commentaires/{id} pour les opérations individuelles. Balises : /balises (GET) et /articles/{id}/balises pour assigner/dissocier. Pour éviter des appels multiples, on peut permettre d'inclure l'auteur ou les balises via ?include=auteur,balises. Il faut aussi gérer la pagination (/articles?page=1&size=20) et le filtrage par date ou catégorie. Les relations many-to-many (articles-tags) peuvent être gérées avec une ressource de liaison (ex: /liaisons_articles_balises) ou directement via PUT sur /articles/{id}/balises. Un piège courant est de créer des URIs trop complexes ; privilégier une structure plate avec des paramètres pour la flexibilité. L'usage de HATEOAS peut aider à rendre l'API découvrable, mais complexifie la conception.
- Étant donné une API CRUD d'utilisateurs, comment implémenteriez-vous les suppressions logiques et la récupération des utilisateurs actifs et supprimés ?Ce qu'une bonne réponse couvre
- Ajouter un champ deleted_at (nullable) à la table users.
- Pour les suppressions logiques, transformer DELETE en UPDATE de deleted_at.
- Les endpoints de listing doivent accepter un paramètre ?status=active|deleted|all pour filtrer.
- Implémenter un endpoint /users/restore/{id} pour récupérer un utilisateur supprimé (en fixant deleted_at à null).
- Attention aux contraintes d'unicité et aux index sur les champs fréquemment filtrés.
Voir un exemple de réponse
Pour implémenter des suppressions logiques, on ajoute une colonne deleted_at timestamp nullable dans la table utilisateur. L'opération DELETE n'efface pas la ligne mais met à jour deleted_at avec l'horodatage courant. Les requêtes GET standard (ex: /users) doivent filtrer WHERE deleted_at IS NULL pour ne renvoyer que les actifs. Pour permettre l'accès aux supprimés, on ajoute un paramètre de requête : GET /users?status=all inclut les utilisateurs dont deleted_at IS NOT NULL, status=deleted ne renvoie que ceux-là. La récupération se fait via un endpoint dédié POST /users/{id}/restore qui efface deleted_at. Un piège est la gestion des contraintes d'unicité (ex: email) : pour éviter les conflits, soit on utilise un index unique partiel (UNIQUE ... WHERE deleted_at IS NULL), soit on autorise les doublons avec un soft delete. Il faut aussi mettre des index sur deleted_at pour les performances des filtres. Cette approche permet une récupération facile et un historique, mais alourdit les requêtes et peut poser problème si le volume de données supprimées est élevé.
- Comment concevriez-vous la pagination pour une grande collection de tweets ? Comparez la pagination par décalage vs basée sur curseur et choisissez-en une.Ce qu'une bonne réponse couvre
- Comparaison offset/limit vs curseur (page basée sur un token).
- Offset est simple mais problématique pour les grandes collections (sauts, doublons si insertion).
- Cursor-based pagination utilise un champ stable (timestamp, ID) pour garantir cohérence.
- Pour des tweets ordonnés par date de création décroissante, utiliser cursor sur id ou created_at.
- Retourner un next_cursor dans la réponse pour permettre la navigation.
Voir un exemple de réponse
Pour paginer une grande collection de tweets, la pagination par curseur (cursor-based) est préférable à l'offset. L'offset est simple (/?page=2&size=20) mais a des inconvénients : si des tweets sont ajoutés entre deux requêtes, les résultats peuvent se décaler (doublons ou sauts). De plus, pour de grandes collections, l'offset force la base de données à parcourir les lignes précédentes, ce qui est lent. La pagination par curseur utilise un champ stable comme un identifiant unique ou un timestamp. Par exemple, on retourne un paramètre ?cursor=id_du_dernier_tweet dans la réponse (sous forme de token encodé). Le serveur exécute alors SELECT ... WHERE id < cursor ORDER BY id DESC LIMIT size. Cette méthode est plus performante et évite les anomalies de données en mouvement. Cependant, elle est moins flexible (pas de saut direct à une page) et nécessite un champ séquentiel unique. Pour les tweets, l'ID auto-incrémenté ou un horodatage fonctionne bien. On renvoie aussi un next_cursor et un previous_cursor dans la réponse JSON. On peut combiner avec du caching côté client pour les tokens.
- Concevez une API qui permet aux clients de demander des champs spécifiques (jeux de champs partiels) et d'inclure des ressources connexes (par exemple, des articles avec les détails de l'auteur).Ce qu'une bonne réponse couvre
- Utiliser un paramètre ?fields= pour spécifier les champs souhaités (ex: /articles?fields=id,titre,contenu).
- Pour l'inclusion de ressources liées, utiliser ?include=auteur,commentaires (ou embed).
- Implémenter side-loading pour éviter le problème N+1 : inclure les relations comme des sections séparées dans la réponse (JSON:API).
- Gérer la validation des champs demandés (retourner 400 si champ inconnu) et la limitation de la profondeur d'inclusion.
- Optimiser les performances en ne chargeant que les colonnes nécessaires en base de données.
Voir un exemple de réponse
Pour permettre aux clients de demander des champs spécifiques, on ajoute un paramètre de requête ?fields[articles]=id,titre,contenu (syntaxe de JSON:API) ou plus simplement ?fields=id,titre,contenu. Le serveur filtre les attributs renvoyés pour chaque ressource, réduisant la bande passante. Pour l'inclusion de ressources connexes, un paramètre ?include=auteur,commentaires permet d'ajouter les objets liés dans la réponse. On peut les placer dans un objet ‘included’ séparé (sparse fieldsets) pour éviter la duplication et faciliter le cache. L'implémentation doit vérifier que les champs et inclusions demandés sont valides, et limiter la profondeur (ex: max 2 niveaux) pour éviter des requêtes complexes. Côté base de données, on sélectionne uniquement les colonnes nécessaires et on effectue des eager loading pour les relations incluses. Un piège est l'explosion de la taille de réponse si on inclut trop de ressources ; prévoir un seuil ou un mode par défaut limité. L'API peut aussi offrir un endpoint séparé pour les détails complets (ex: /articles/{id}/details).
- Comment versionneriez-vous une API REST ? Expliquez les avantages et inconvénients du versioning par URI vs versioning par en-tête de requête personnalisé.Ce qu'une bonne réponse couvre
- Versioning via URI : /v1/utilisateurs, /v2/utilisateurs.
- Avantages : explicite, facile à router, visible dans les logs.
- Inconvénients : duplication de code, rupture de la découverte HATEOAS, pollution des URIs.
- Versioning via en-tête personnalisé : Accept-Version: 1 ou Header X-API-Version.
- Avantages : URIs propres, version par défaut possible, extension plus facile.
- Inconvénients : moins visible, nécessite des négociations de contenu, courbe d'apprentissage pour clients.
Voir un exemple de réponse
Le versioning par URI (ex: /v1/utilisateurs) est le plus courant car il est explicite et facile à implémenter : le routeur peut diriger les requêtes vers des contrôleurs ou versions différentes. Il est clair pour les développeurs et les logs. Cependant, il peut mener à du code dupliqué (chaque version peut être une copie presque identique), et peut compliquer l'implémentation du HATEOAS (les liens doivent inclure la version). De plus, il pollue l'URI avec des détails d'implémentation. Le versioning par en-tête personnalisé (ex: Accept-Version: 2) garde les URIs propres et permet de basculer la version par défaut sans changement d'URI. Mais il est moins visible, les clients doivent gérer des en-têtes, et le routage côté serveur est plus complexe (middleware pour extraire la version). Il y a aussi le risque que les clients oublient l'en-tête et reçoivent une version par défaut qui peut changer. Une approche mixte peut être utilisée : URI pour les versions majeures (v1, v2) et en-têtes pour les modifications mineures. En pratique, le versioning par URI est souvent recommandé pour les API publiques car il est simple et sans ambiguïté. Cependant, il faut le planifier tôt pour éviter les changements rupturants.
- Écrivez un middleware Express.js simple qui valide une clé API depuis l'en-tête Authorization et retourne une 401 si manquante ou invalide.Ce qu'une bonne réponse couvre
- Middleware Express.js qui vérifie l'en-tête Authorization.
- Extract the API key from the header (format Bearer <key>).
- Compare with a stored key (e.g., environment variable or database).
- Return 401 avec un message d'erreur si manquant ou invalide.
- Appeler next() si valide.
Voir un exemple de réponse
Voici un middleware Express.js qui valide une clé API depuis l'en-tête Authorization. Il extrait la clé du format 'Bearer <api_key>', la compare à une clé de référence (par exemple définie dans une variable d'environnement), et renvoie une réponse 401 'Unauthorized' si la clé est absente ou incorrecte. Sinon, il passe au middleware suivant. Ce middleware doit être appliqué aux routes protégées. Noter que cet exemple est simpliste ; en production, on utiliserait souvent des clés stockées en base ou validées via un service d'authentification. Le code inclut la gestion des erreurs et le retour JSON avec un message clair.
Solution de référencejavascript // middleware/apiKeyAuth.js const API_KEY = process.env.API_KEY || 'my-secret-key'; // ou récupérée d'une base const apiKeyAuth = (req, res, next) => { const authHeader = req.headers['authorization']; if (!authHeader) { return res.status(401).json({ error: 'Clé API manquante dans l\'en-tête Authorization' }); } // Format attendu: 'Bearer <cle>' const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { return res.status(401).json({ error: 'Format Authorization invalide. Utilisez Bearer <clé>' }); } const key = parts[1]; if (key !== API_KEY) { return res.status(401).json({ error: 'Clé API invalide' }); } // Si valide, continuer next(); }; module.exports = apiKeyAuth; // Utilisation dans une route: // const apiKeyAuth = require('./middleware/apiKeyAuth'); // app.use('/api', apiKeyAuth); // Complexité temporelle: O(1) - opérations de chaîne constantes. // Complexité spatiale: O(1). - Concevez une structure de réponse d'erreur pour une API de paiement. Incluez des exemples pour les erreurs de validation, les fonds insuffisants et les erreurs serveur.Ce qu'une bonne réponse couvre
- Structure de réponse d'erreur cohérente avec code HTTP, un code métier, un message lisible.
- Pour les erreurs de validation (400) : inclure les détails du champ en erreur.
- Pour les fonds insuffisants (402 ou 409) : message spécifique, solde requis, montant demandé.
- Pour les erreurs serveur (500) : message générique sans détails internes.
- Respecter une norme comme RFC 7807 (Problem Details for HTTP APIs) pour l'interopérabilité.
Voir un exemple de réponse
Une structure d'erreur cohérente est cruciale pour une API de paiement. On peut suivre le modèle Problem Details (RFC 7807) avec les champs: type, title, status, detail, instance. Pour les erreurs de validation (400), on ajoute un tableau 'errors' avec le champ, le message et le code. Exemple: { "type": "https://api.example.com/errors/validation", "title": "Validation échouée", "status": 400, "detail": "Le champ 'amount' doit être un nombre positif", "instance": "/payments", "errors": [ { "field": "amount", "message": "Montant invalide", "code": "INVALID_AMOUNT" } ] }. Pour fonds insuffisants, utiliser un code 402 (paiement requis) ou 409 (conflit). Exemple: { "type": "https://api.example.com/errors/insufficient-funds", "title": "Fonds insuffisants", "status": 402, "detail": "Solde disponible: 50, montant demandé: 100", "instance": "/payments/123" }. Pour les erreurs serveur (500), on cache les détails internes: { "type": "https://api.example.com/errors/internal", "title": "Erreur interne", "status": 500, "detail": "Une erreur inattendue s'est produite", "instance": "/payments/456" }. Ne jamais exposer de stack trace ou d'informations sensibles. L'inclusion d'un identifiant de trace (corrélation ID) est utile pour le débogage.
- Comment implémenteriez-vous la limitation de débit pour une API publique ? Décrivez l'algorithme et comment vous communiqueriez les limites aux clients.Ce qu'une bonne réponse couvre
- Choisir un algorithme comme le Token Bucket ou le Leaky Bucket pour sa simplicité.
- Implémenter un compteur par clé API utilisant un cache distribué (Redis) avec TTL.
- Définir des limites (ex: 100 requêtes par minute) et les retourner dans les en-têtes de réponse.
- Utiliser des en-têtes standardisés: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
- Retourner 429 Too Many Requests avec un en-tête Retry-After lorsque la limite est dépassée.
Voir un exemple de réponse
Pour limiter le débit, l'algorithme Token Bucket est populaire : chaque client a un seau de jetons qui se remplissent à un taux constant. Chaque requête consomme un jeton ; si le seau est vide, la requête est rejetée. En pratique, on stocke le compteur et le timestamp dans Redis avec une clé par client API. On utilise un script Lua pour atomicité. Les limites (par minute/heure) sont configurées. On communique les limites via des en-têtes: X-RateLimit-Limit (nombre max de requêtes), X-RateLimit-Remaining (restant dans la fenêtre), X-RateLimit-Reset (timestamp de réinitialisation). Les clients doivent lire ces en-têtes et adapter leur comportement. En cas de dépassement, retourner 429 avec Retry-After en secondes. Il faut aussi gérer les bursts (ex: permettre un pic court) en ajustant la taille du seau. Un piège est la scalabilité : utiliser Redis cluster ou un cache distribué pour éviter les goulots d'étranglement. On peut aussi implémenter un algorithme plus simple comme Fixed Window (compteur reset toutes les minutes) mais il est sujet aux bursts en fin de fenêtre. Sliding Window Log est plus précis mais plus coûteux.
Comment se préparer
- Commencez toujours par identifier les ressources et leurs relations, puis mappez-les aux points de terminaison avec les méthodes HTTP appropriées.
- Simulez une API simple dans votre langage préféré (par exemple, Node.js/Express, Python/Flask) pour pratiquer les opérations CRUD et la gestion des erreurs.
- Étudiez les motifs de conception d'API courants comme HATEOAS, l'idempotence pour PUT/DELETE, et les méthodes sûres/idempotentes.
- Préparez-vous à discuter des compromis : par exemple, pourquoi vous choisiriez une approche de pagination ou une stratégie de versioning particulière.
- Entraînez-vous au tableau blanc : expliquez clairement vos décisions de conception pour le nommage des ressources, les codes d'état et les formats d'erreur.
Questions fréquemment posées
Quelle est l'erreur la plus courante dans la conception d'API REST ?
Utiliser des verbes dans les points de terminaison (par exemple, /users/getUser au lieu de GET /users/:id). Aussi, ne pas utiliser les codes d'état HTTP appropriés pour les erreurs.
Dois-je toujours utiliser HATEOAS ?
HATEOAS est une contrainte pour une adhérence totale à REST, mais pour les microservices pratiques, il est souvent omis pour simplifier. Concentrez-vous sur des liens de ressources clairs.
Comment gérer les mises à jour partielles ?
Utilisez PATCH avec un format JSON Patch ou Merge Patch. Pour les mises à jour partielles, incluez uniquement les champs à modifier dans le corps de la requête.
Quelle est la meilleure façon de versionner une API ?
Le versioning par URI (par exemple, /v1/users) est explicite et facile à mettre en cache, mais le versioning par en-tête garde les URL propres. Choisissez en fonction des préférences de votre équipe.
Comment tester un entretien de conception d'API REST ?
Entraînez-vous avec des entretiens simulés sur un tableau blanc ou en utilisant des outils comme Postman. Concentrez-vous sur l'explication des compromis et de votre processus de réflexion.
Pratiquez les questions REST API design avec des retours instantanés de l'IA
Téléchargez votre CV, obtenez un entretien simulé personnalisé et voyez exactement ce qu'il faut améliorer — gratuit pour commencer.