Meilleures pratiques pour la création d'API REST

salut!



Cet article, malgré son titre innocent, a provoqué une discussion tellement verbeuse sur Stackoverflow que nous ne pouvions pas passer outre. Une tentative pour saisir l'immensité - pour parler clairement de la conception compétente de l'API REST - apparemment, l'auteur a réussi à bien des égards, mais pas complÚtement. En tout cas, nous espérons rivaliser avec l'original dans le degré de discussion, ainsi que le fait que nous rejoindrons l'armée de fans d'Express.



Bonne lecture!



Les API REST sont l'un des types de services Web les plus courants disponibles aujourd'hui. Avec leur aide, divers clients, y compris des applications de navigateur, peuvent Ă©changer des informations avec le serveur via l'API REST.



Par conséquent, il est trÚs important de concevoir correctement l'API REST afin de ne pas avoir de problÚmes en cours de route. Considérez la sécurité, les performances et la convivialité de l'API du point de vue du consommateur.



Sinon, nous provoquerons des problÚmes pour les clients utilisant nos API - ce qui est frustrant et ennuyeux. Si nous ne respectons pas les conventions communes, nous ne ferons que confondre ceux qui maintiendront notre API, ainsi que les clients, car l'architecture sera différente de celle que tout le monde s'attend à voir.



Cet article examinera comment concevoir des API REST de maniĂšre Ă  ce qu'elles soient simples et comprĂ©hensibles pour tous ceux qui les utilisent. Nous assurerons leur pĂ©rennitĂ©, leur sĂ©curitĂ© et leur rapiditĂ©, car les donnĂ©es transfĂ©rĂ©es aux clients via une telle API peuvent ĂȘtre confidentielles.



Puisqu'il existe de nombreuses raisons et options pour qu'une application réseau échoue, nous devons nous assurer que les erreurs dans toute API REST sont gérées avec élégance et accompagnées de codes HTTP standard pour aider le consommateur à résoudre le problÚme.



Acceptez JSON et renvoyez JSON en réponse



Les API REST doivent accepter JSON pour la charge utile de la requĂȘte et Ă©galement envoyer des rĂ©ponses JSON. JSON est une norme de transfert de donnĂ©es. Presque toutes les technologies rĂ©seau sont adaptĂ©es pour l'utiliser: JavaScript a des mĂ©thodes intĂ©grĂ©es pour encoder et dĂ©coder JSON, soit via l'API Fetch, soit via un autre client HTTP. Les technologies cĂŽtĂ© serveur utilisent des bibliothĂšques pour dĂ©coder JSON avec peu ou pas d'intervention de votre part.



Il existe d'autres moyens de transférer des données. XML en tant que tel n'est pas trÚs largement pris en charge dans les frameworks; vous devez généralement convertir les données dans un format plus pratique, qui est généralement JSON. CÎté client, en particulier dans le navigateur, il n'est pas si simple de traiter ces données. Vous devez faire beaucoup de travail supplémentaire juste pour assurer le transfert normal des données.



Les formulaires sont pratiques pour transférer des données, surtout si nous allons transférer des fichiers. Mais pour transférer des informations sous forme de texte et numérique, vous pouvez vous passer de formulaires, car la plupart des frameworks permettent le transfert JSON sans traitement supplémentaire - prenez simplement les données cÎté client. C'est la maniÚre la plus simple de les gérer.



Pour garantir que le client interprĂšte le JSON reçu de notre API REST exactement comme JSON, Content-Typel'en-tĂȘte de rĂ©ponse doit ĂȘtre dĂ©fini sur une valeur une application/jsonfois la demande effectuĂ©e. De nombreux frameworks d'application cĂŽtĂ© serveur dĂ©finissent automatiquement l'en-tĂȘte de rĂ©ponse. Certains clients HTTP regardent Content-Typedans l'en-tĂȘte de la rĂ©ponse et analysent les donnĂ©es selon le format qui y est spĂ©cifiĂ©.



La seule exception se produit lorsque nous essayons d'envoyer et de recevoir des fichiers qui sont transférés entre le client et le serveur. Ensuite, vous devez traiter les fichiers reçus en tant que réponse et envoyer les données du formulaire du client au serveur. Mais c'est un sujet pour un autre article.



Nous devons également nous assurer que JSON est la réponse de nos points de terminaison. De nombreux frameworks de serveur intÚgrent cette fonctionnalité.



Prenons un exemple d'API qui accepte une charge utile JSON. Cet exemple utilise le framework de backend Express pour Node.js. Nous pouvons utiliser un programme comme middleware body-parserpour analyser le corps de la requĂȘte JSON, puis appeler une mĂ©thode res.jsonavec l'objet que nous voulons renvoyer en tant que rĂ©ponse JSON. Ceci est fait comme ceci:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.post('/', (req, res) => {
  res.json(req.body);
});

app.listen(3000, () => console.log('server started'));


bodyParser.json()analyse la chaßne du corps de la demande en JSON, la convertit en objet JavaScript, puis attribue le résultat à l'objet req.body.



DĂ©finissez l'en-tĂȘte Content-Type dans la rĂ©ponse sur une valeur application/json; charset=utf-8sans aucune modification. La mĂ©thode prĂ©sentĂ©e ci-dessus est applicable Ă  la plupart des autres frameworks backend.



Nous utilisons des noms pour les chemins vers les points de terminaison, pas des verbes



Les noms des chemins vers les points de terminaison ne doivent pas ĂȘtre des verbes, mais des noms. Ce nom reprĂ©sente l'objet du point de terminaison, que nous rĂ©cupĂ©rons Ă  partir de lĂ , ou que nous manipulons.



Le fait est que le nom de notre mĂ©thode de requĂȘte HTTP contient dĂ©jĂ  un verbe. Mettre des verbes dans les noms des chemins vers le point de terminaison de l'API n'est pas pratique; de plus, le nom s'avĂšre inutilement long et ne contient aucune information valable. Les verbes choisis par le dĂ©veloppeur peuvent ĂȘtre mis simplement en fonction de son caprice. Par exemple, certaines personnes prĂ©fĂšrent l'option 'get', et d'autres prĂ©fĂšrent 'retrieve', il est donc prĂ©fĂ©rable de vous limiter au verbe HTTP GET familier qui vous indique ce que fait le point de terminaison.



L'action doit ĂȘtre spĂ©cifiĂ©e dans le nom de la mĂ©thode HTTP de la requĂȘte que nous faisons. Les mĂ©thodes les plus courantes contiennent les verbes GET, POST, PUT et DELETE.

GET récupÚre les ressources. POST envoie de nouvelles données au serveur. PUT met à jour les données existantes. DELETE supprime les données. Chacun de ces verbes correspond à l'une des opérations du groupe CRUD .



Compte tenu des deux principes Ă©voquĂ©s ci-dessus, afin de recevoir de nouveaux articles, nous devons crĂ©er des itinĂ©raires de la forme GET /articles/. De mĂȘme, nous utilisons POST /articles/pour mettre Ă  jour un nouvel article, PUT /articles/:id pour mettre Ă  jour un article avec celui donnĂ© id. La mĂ©thode DELETE est /articles/:idconçue pour supprimer un article avec un ID donnĂ©.



/articlesEst une ressource API REST. Par exemple, vous pouvez utiliser Express pour effectuer les opérations suivantes avec des articles:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles', (req, res) => {
  const articles = [];
  //    ...
  res.json(articles);
});

app.post('/articles', (req, res) => {
  //     ...
  res.json(req.body);
});

app.put('/articles/:id', (req, res) => {
  const { id } = req.params;
  //    ...
  res.json(req.body);
});

app.delete('/articles/:id', (req, res) => {
  const { id } = req.params;
  //    ...
  res.json({ deleted: id });
});

app.listen(3000, () => console.log('server started'));


Dans le code ci-dessus, nous avons défini des points de terminaison pour manipuler les articles. Comme vous pouvez le voir, il n'y a pas de verbes dans les noms de chemin. Noms seulement. Les verbes ne sont utilisés que dans les noms des méthodes HTTP.



Les points de terminaison POST, PUT et DELETE acceptent un corps de requĂȘte JSON et renvoient une rĂ©ponse JSON, y compris un point de terminaison GET.



Les collections sont appelées noms pluriels



Les collections doivent ĂȘtre nommĂ©es avec des noms pluriels. Ce n'est pas souvent que nous devons prendre un seul Ă©lĂ©ment d'une collection, nous devons donc ĂȘtre cohĂ©rents et utiliser des noms pluriels dans les noms de collection.



Le pluriel est également utilisé pour assurer la cohérence avec les conventions de dénomination dans les bases de données. En rÚgle générale, une table ne contient pas un, mais plusieurs enregistrements et la table est nommée en conséquence.



Lorsque vous travaillez avec un point de terminaison, /articlesnous utilisons le pluriel pour nommer tous les points de terminaison.



Imbrication de ressources lors de l'utilisation d'objets hiérarchiques



Le chemin des points de terminaison traitant des ressources imbriquĂ©es doit ĂȘtre structurĂ© comme ceci: ajoutez la ressource imbriquĂ©e comme chemin d'accĂšs aprĂšs le nom de la ressource parente.

Vous devez vous assurer que dans votre code, l'imbrication des ressources correspond exactement à l'imbrication des informations dans nos tables de base de données. Sinon, la confusion est possible.



Par exemple, si nous voulons recevoir des commentaires sur un nouvel article à un certain point de terminaison, nous devons attacher le chemin / les commentaires à la fin du chemin /articles. Dans ce cas, on suppose que nous considérons l'entité de commentaires comme une entité enfant articledans notre base de données.



Par exemple, vous pouvez le faire avec le code suivant dans Express:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles/:articleId/comments', (req, res) => {
  const { articleId } = req.params;
  const comments = [];
  //      articleId
  res.json(comments);
});


app.listen(3000, () => console.log('server started'));


Dans le code ci-dessus, vous pouvez utiliser la méthode GET sur le chemin '/articles/:articleId/comments'. Nous recevons des commentaires commentssur l'article correspondant articleId, puis nous le renvoyons en réponse. Nous ajoutons 'comments'aprÚs le segment de chemin '/articles/:articleId'pour indiquer qu'il s'agit d'une ressource enfant /articles.



Cela a du sens car les commentaires sont des objets enfants articleset il est supposĂ© que chaque article a son propre ensemble de commentaires. Sinon, cette structure peut prĂȘter Ă  confusion pour l'utilisateur, car elle est gĂ©nĂ©ralement utilisĂ©e pour accĂ©der aux objets enfants. Le mĂȘme principe s'applique lorsque vous travaillez avec des points de terminaison POST, PUT et DELETE. Ils utilisent tous la mĂȘme structure d'imbrication lors de la construction des noms de chemin.



Traitement soigné des erreurs et renvoyer les codes d'erreur standard



Pour Ă©viter toute confusion lorsqu'une erreur se produit sur l'API, gĂ©rez les erreurs avec soin et renvoyez des codes de rĂ©ponse HTTP indiquant quelle erreur s'est produite. Cela fournit aux responsables de l'API des informations suffisantes pour comprendre le problĂšme. Il est inacceptable que des erreurs plantent le systĂšme, par consĂ©quent, elles ne peuvent pas ĂȘtre laissĂ©es sans traitement, et le consommateur d'API doit faire face Ă  un tel traitement.



Les codes d'erreur HTTP les plus courants sont:



  • 400 Bad Request - Indique que l'entrĂ©e reçue du client a Ă©chouĂ© Ă  la validation.
  • 401 Non autorisĂ© - signifie que l'utilisateur ne s'est pas connectĂ© et n'a donc pas l'autorisation d'accĂ©der Ă  la ressource. En rĂšgle gĂ©nĂ©rale, ce code est Ă©mis lorsque l'utilisateur n'est pas authentifiĂ©.
  • 403 Interdit - Indique que l'utilisateur est authentifiĂ© mais n'est pas autorisĂ© Ă  accĂ©der Ă  la ressource.
  • 404 Not Found - signifie que la ressource n'a pas Ă©tĂ© trouvĂ©e
  • L'erreur de serveur interne 500 est une erreur de serveur et ne devrait probablement pas ĂȘtre renvoyĂ©e explicitement.
  • 502 Bad Gateway - Indique un message de rĂ©ponse non valide du serveur en amont.
  • 503 Service indisponible - signifie que quelque chose d'inattendu s'est produit cĂŽtĂ© serveur - par exemple, surcharge du serveur, dĂ©faillance de certains Ă©lĂ©ments du systĂšme, etc.


Vous devez Ă©mettre exactement les codes qui correspondent Ă  l'erreur qui a empĂȘchĂ© notre application. Par exemple, si nous voulons rejeter les donnĂ©es reçues en tant que charge utile de requĂȘte, alors, conformĂ©ment aux rĂšgles de l'API Express, nous devons renvoyer un code de 400:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

//  
const users = [
  { email: 'abc@foo.com' }
]

app.use(bodyParser.json());

app.post('/users', (req, res) => {
  const { email } = req.body;
  const userExists = users.find(u => u.email === email);
  if (userExists) {
    return res.status(400).json({ error: 'User already exists' })
  }
  res.json(req.body);
});


app.listen(3000, () => console.log('server started'));


Dans le code ci-dessus, nous conservons dans le tableau des utilisateurs une liste d'utilisateurs existants qui ont des e-mails connus.



De plus, si nous essayons d'envoyer une charge utile avec une valeur emaildéjà présente dans les utilisateurs, nous obtenons une réponse avec un code de 400 et un message 'User already exists'indiquant qu'un tel utilisateur existe déjà. Avec ces informations, l'utilisateur peut s'améliorer - remplacez l'adresse e-mail par celle qui ne figure pas encore sur la liste.



Les codes d'erreur doivent toujours ĂȘtre accompagnĂ©s de messages suffisamment informatifs pour corriger l'erreur, mais pas suffisamment dĂ©taillĂ©s pour que ces informations puissent ĂȘtre utilisĂ©es par des attaquants qui ont l'intention de voler nos informations ou de planter le systĂšme.



Chaque fois que notre API ne parvient pas Ă  s'arrĂȘter correctement, nous devons gĂ©rer soigneusement l'Ă©chec en envoyant des informations d'erreur pour faciliter la correction de la situation par l'utilisateur.



Autoriser le tri, le filtrage et la pagination des données



Les bases derriĂšre l'API REST peuvent beaucoup Ă©voluer. Parfois, il y a tellement de donnĂ©es qu'il est impossible de les rĂ©cupĂ©rer toutes en mĂȘme temps, car cela ralentira le systĂšme ou mĂȘme le fera tomber. Par consĂ©quent, nous avons besoin d'un moyen de filtrer les Ă©lĂ©ments.



Nous avons également besoin de moyens pour paginer les données (pagination) afin de ne renvoyer que quelques résultats à la fois. Nous ne voulons pas prendre trop de temps sur les ressources pour essayer d'extraire toutes les données demandées à la fois.



Le filtrage et la pagination des données peuvent améliorer les performances en réduisant l'utilisation des ressources du serveur. Plus les données s'accumulent dans la base de données, plus ces deux possibilités deviennent importantes.



Voici un petit exemple oĂč l'API peut accepter une chaĂźne de requĂȘte avec divers paramĂštres. Filtrons les Ă©lĂ©ments par leurs champs:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

//      
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  const { firstName, lastName, age } = req.query;
  let results = [...employees];
  if (firstName) {
    results = results.filter(r => r.firstName === firstName);
  }

  if (lastName) {
    results = results.filter(r => r.lastName === lastName);
  }

  if (age) {
    results = results.filter(r => +r.age === +age);
  }
  res.json(results);
});

app.listen(3000, () => console.log('server started'));


Dans le code ci-dessus, nous avons une variable req.queryqui nous permet d'obtenir des paramĂštres de requĂȘte. Nous pouvons ensuite extraire les valeurs de propriĂ©tĂ© en dĂ©structurant les paramĂštres de requĂȘte individuels en variables; JavaScript a une syntaxe spĂ©ciale pour cela.



Enfin, nous appliquons un filtre sur chaque valeur de paramĂštre de requĂȘte pour trouver les Ă©lĂ©ments que nous voulons retourner.



Cela fait, nous renvoyons les rĂ©sultats en tant que rĂ©ponse. Par consĂ©quent, lorsque vous effectuez une requĂȘte GET vers le chemin suivant avec une chaĂźne de requĂȘte:



/employees?lastName=Smith&age=30


On a:



[
    {
        "firstName": "John",
        "lastName": "Smith",
        "age": 30
    }
]


comme réponse renvoyée car le filtrage était activé lastNameet age.



De mĂȘme, vous pouvez accepter le paramĂštre de requĂȘte de page et renvoyer un groupe d'enregistrements occupant des positions de (page - 1) * 20Ă  page * 20.



Également dans la chaĂźne de requĂȘte, vous pouvez spĂ©cifier les champs par lesquels le tri sera effectuĂ©. Dans ce cas, nous pouvons les trier par ces champs sĂ©parĂ©s. Par exemple, nous pouvons avoir besoin d'extraire une chaĂźne de requĂȘte d'une URL comme celle-ci:



http://example.com/articles?sort=+author,-datepublished


OĂč +signifie «haut» et –«bas». Nous trions donc par nom d'auteur par ordre alphabĂ©tique et par date de publication du plus rĂ©cent au plus ancien.



Adhérez à des pratiques de sécurité éprouvées



La communication entre le client et le serveur doit ĂȘtre principalement privĂ©e, car nous envoyons et recevons souvent des informations confidentielles. Par consĂ©quent, l'utilisation de SSL / TLS pour la sĂ©curitĂ© est un must.



Le certificat SSL n'est pas si difficile Ă  tĂ©lĂ©charger sur le serveur, et le certificat lui-mĂȘme est soit gratuit, soit trĂšs bon marchĂ©. Il n'y a aucune raison de renoncer Ă  permettre Ă  nos API REST de communiquer sur des canaux sĂ©curisĂ©s plutĂŽt que sur des canaux ouverts.



Une personne ne doit pas avoir accÚs à plus d'informations que ce qu'elle a demandé. Par exemple, un utilisateur ordinaire ne devrait pas avoir accÚs aux informations d'un autre utilisateur. De plus, il ne devrait pas pouvoir afficher les données des administrateurs.



Pour promouvoir le principe du moindre privilÚge, vous devez soit implémenter la vérification des rÎles pour un rÎle spécifique, soit fournir une plus grande granularité des rÎles pour chaque utilisateur.



Si nous dĂ©cidons de regrouper les utilisateurs en plusieurs rĂŽles, les rĂŽles doivent ĂȘtre dotĂ©s de tels droits d'accĂšs qui garantissent que tout ce dont l'utilisateur a besoin est fait et rien de plus. Si nous prescrivons plus en dĂ©tail les droits d'accĂšs Ă  chaque opportunitĂ© fournie Ă  l'utilisateur, nous devons nous assurer que l'administrateur peut accorder ces capacitĂ©s Ă  n'importe quel utilisateur ou retirer ces capacitĂ©s. En outre, vous devez ajouter des rĂŽles prĂ©dĂ©finis pouvant ĂȘtre appliquĂ©s Ă  un groupe d'utilisateurs afin de ne pas avoir Ă  dĂ©finir manuellement les droits nĂ©cessaires pour chaque utilisateur.



Mettez en cache les données pour améliorer les performances



La mise en cache peut ĂȘtre ajoutĂ©e pour renvoyer des donnĂ©es Ă  partir d'une mĂ©moire cache locale, plutĂŽt que de rĂ©cupĂ©rer certaines donnĂ©es de la base de donnĂ©es chaque fois que les utilisateurs le demandent. L'avantage de la mise en cache est que les utilisateurs peuvent rĂ©cupĂ©rer les donnĂ©es plus rapidement. Cependant, ces donnĂ©es peuvent ĂȘtre obsolĂštes. Cela peut Ă©galement ĂȘtre source de problĂšmes lors du dĂ©bogage dans les environnements de production, lorsque quelque chose ne va pas et que nous continuons Ă  examiner les anciennes donnĂ©es.



Il existe une variété d'options de mise en cache disponibles, telles que Redis , la mise en cache en mémoire, etc. Vous pouvez modifier la façon dont les données sont mises en cache selon vos besoins.



Par exemple, Express fournit un middlewareapicachepour ajouter une capacitĂ© de mise en cache Ă  votre application sans configuration compliquĂ©e. Une simple mise en cache en mĂ©moire peut ĂȘtre ajoutĂ©e au serveur comme ceci:



const express = require('express');

const bodyParser = require('body-parser');
const apicache = require('apicache');
const app = express();
let cache = apicache.middleware;
app.use(cache('5 minutes'));

//      
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


Le code ci-dessus fait simplement référence à apicacheavec apicache.middleware, ce qui donne:



app.use(cache('5 minutes'))


et cela suffit pour appliquer la mise en cache Ă  l'Ă©chelle de l'application. Nous mettons en cache, par exemple, tous les rĂ©sultats en cinq minutes. Par la suite, cette valeur peut ĂȘtre ajustĂ©e en fonction de ce dont nous avons besoin.



Gestion des versions d'API



Nous avons besoin de diffĂ©rentes versions de l'API au cas oĂč nous y apporterions des modifications susceptibles de perturber le client. Le versionnage peut ĂȘtre effectuĂ© sur une base sĂ©mantique (par exemple, 2.0.6 signifie que la version principale est 2, et c'est le sixiĂšme patch). Ce principe est dĂ©sormais acceptĂ© dans la plupart des applications.



De cette façon, vous pouvez progressivement retirer les anciens points de terminaison plutĂŽt que de forcer tout le monde Ă  basculer simultanĂ©ment vers la nouvelle API. Vous pouvez enregistrer la version v1 pour ceux qui ne veulent rien changer, et fournir la version v2 avec toutes ses nouvelles fonctionnalitĂ©s pour ceux qui sont prĂȘts Ă  la mise Ă  niveau. Ceci est particuliĂšrement important dans le contexte des API publiques. Ils doivent ĂȘtre versionnĂ©s pour Ă©viter de casser les applications tierces qui utilisent nos API.



Le contrÎle de version est généralement effectué en ajoutant /v1/,/v2/, etc., ajouté au début du chemin de l'API.



Par exemple, voici comment procéder dans Express:



const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.get('/v1/employees', (req, res) => {
  const employees = [];
  //      
  res.json(employees);
});

app.get('/v2/employees', (req, res) => {
  const employees = [];
  //       
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


Nous ajoutons simplement le numéro de version au début du chemin menant au point de terminaison.



Conclusion



La chose la plus importante à retenir de la conception d'API REST de haute qualité est de maintenir la cohérence en suivant les normes et les conventions du Web. Les codes d'état JSON, SSL / TLS et HTTP sont aujourd'hui incontournables sur le Web.



La performance est tout aussi importante. Vous pouvez l'augmenter sans renvoyer trop de donnĂ©es Ă  la fois. De plus, vous pouvez utiliser la mise en cache pour Ă©viter de demander sans cesse les mĂȘmes donnĂ©es.



Les chemins d'accĂšs aux points de terminaison doivent ĂȘtre nommĂ©s de maniĂšre cohĂ©rente. Vous devez utiliser des noms dans leurs noms, car les verbes sont prĂ©sents dans les noms des mĂ©thodes HTTP. Les chemins d'accĂšs aux ressources imbriquĂ©es doivent suivre le chemin d'accĂšs aux ressources parent. Ils doivent communiquer ce que nous recevons ou manipulons, afin que nous n'ayons pas Ă  consulter en plus la documentation pour comprendre ce qui se passe.



All Articles