Best Practices zum Erstellen von REST-APIs

Hallo!



Dieser Artikel provozierte trotz seines unschuldigen Titels eine so ausführliche Diskussion über Stackoverflow, dass wir ihn nicht passieren konnten. Der Versuch, die Unermesslichkeit zu erfassen - um klar über das kompetente Design der REST-API zu berichten -, gelang dem Autor anscheinend in vielerlei Hinsicht, aber nicht vollständig. In jedem Fall hoffen wir, im Grad der Diskussion mit dem Original mithalten zu können und dass wir uns der Armee der Express-Fans anschließen werden.



Viel Spaß beim Lesen!



REST-APIs sind eine der häufigsten Arten von Webdiensten, die heute verfügbar sind. Mit ihrer Hilfe können verschiedene Clients, einschließlich Browseranwendungen, Informationen über die REST-API mit dem Server austauschen.



Daher ist es sehr wichtig, die REST-API korrekt zu gestalten, damit Sie unterwegs keine Probleme haben. Berücksichtigen Sie die Sicherheit, Leistung und Benutzerfreundlichkeit der API aus Verbrauchersicht.



Andernfalls werden wir Kunden, die unsere API verwenden, Probleme bereiten - was frustrierend und ärgerlich ist. Wenn wir nicht den üblichen Konventionen folgen, werden wir nur diejenigen verwirren, die unsere API pflegen, sowie die Kunden, da sich die Architektur von der unterscheidet, die jeder erwartet.



In diesem Artikel wird erläutert, wie REST-APIs so gestaltet werden, dass sie für jeden Benutzer einfach und verständlich sind. Wir werden ihre Haltbarkeit, Sicherheit und Geschwindigkeit sicherstellen, da die über eine solche API an Kunden übertragenen Daten vertraulich sein können.



Da es viele Gründe und Optionen für den Ausfall einer Netzwerkanwendung gibt, müssen wir sicherstellen, dass Fehler in einer REST-API ordnungsgemäß behandelt und von Standard-HTTP-Codes begleitet werden, damit der Verbraucher das Problem besser lösen kann.



Akzeptieren Sie JSON und geben Sie JSON als Antwort zurück



REST-APIs müssen JSON für die Anforderungsnutzlast akzeptieren und auch JSON-Antworten senden. JSON ist ein Datenübertragungsstandard. Fast jede Netzwerktechnologie ist für die Verwendung angepasst: JavaScript verfügt über integrierte Methoden zum Codieren und Decodieren von JSON, entweder über die Fetch-API oder über einen anderen HTTP-Client. Serverseitige Technologien verwenden Bibliotheken, um JSON zu dekodieren, ohne dass Sie eingreifen müssen.



Es gibt andere Möglichkeiten, Daten zu übertragen. XML selbst wird in Frameworks nicht sehr häufig unterstützt. Normalerweise müssen Sie die Daten in ein bequemeres Format konvertieren, das normalerweise JSON ist. Auf der Client-Seite, insbesondere im Browser, ist es nicht so einfach, mit diesen Daten umzugehen. Sie müssen viel zusätzliche Arbeit leisten, um die normale Datenübertragung sicherzustellen.



Formulare eignen sich zum Übertragen von Daten, insbesondere wenn Dateien übertragen werden sollen. Für die Übertragung von Informationen in Text- und numerischer Form können Sie jedoch auf Formulare verzichten, da die meisten Frameworks die JSON-Übertragung ohne zusätzliche Verarbeitung ermöglichen. Nehmen Sie einfach die Daten auf der Clientseite. Dies ist der einfachste Weg, mit ihnen umzugehen.



Um sicherzustellen, dass der Client den von unserer REST-API empfangenen JSON genau als JSON interpretiert, setzen Sie Content-Typeden Antwortheader application/jsonnach der Anforderung auf einen Wert . Viele serverseitige Anwendungsframeworks setzen den Antwortheader automatisch. Einige HTTP-Clients überprüfen Content-Typeden Antwortheader und analysieren die Daten gemäß dem dort angegebenen Format.



Die einzige Ausnahme tritt auf, wenn wir versuchen, Dateien zu senden und zu empfangen, die zwischen dem Client und dem Server übertragen werden. Anschließend müssen Sie die als Antwort empfangenen Dateien verarbeiten und die Formulardaten vom Client an den Server senden. Dies ist jedoch ein Thema für einen anderen Artikel.



Wir müssen auch sicherstellen, dass JSON die Antwort von unseren Endpunkten ist. In vielen Server-Frameworks ist diese Funktion integriert.



Nehmen wir ein Beispiel für eine API, die eine JSON-Nutzlast akzeptiert. In diesem Beispiel wird das Express- Backend-Framework für Node.js verwendet. Wir können ein Programm als Middleware verwenden body-parser, um den JSON-Anforderungshauptteil zu analysieren, und dann eine Methode res.jsonmit dem Objekt aufrufen, das wir als JSON-Antwort zurückgeben möchten. Dies geschieht folgendermaßen:



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()Analysiert die Zeichenfolge des Anforderungshauptteils in JSON, konvertiert sie in ein JavaScript-Objekt und weist das Ergebnis dann dem Objekt zu req.body.



Setzen Sie den Content-Type-Header in der Antwort auf einen Wert application/json; charset=utf-8ohne Änderungen. Die oben gezeigte Methode ist auf die meisten anderen Backend-Frameworks anwendbar.



Wir verwenden Namen für Pfade zu Endpunkten, keine Verben



Die Namen der Pfade zu Endpunkten sollten keine Verben sein, sondern Namen. Dieser Name repräsentiert das Objekt vom Endpunkt, das wir von dort abrufen oder das wir manipulieren.



Tatsache ist, dass der Name unserer HTTP-Anforderungsmethode bereits ein Verb enthält. Das Einfügen von Verben in die Namen der Pfade zum API-Endpunkt ist unpraktisch. Darüber hinaus ist der Name unnötig lang und enthält keine wertvollen Informationen. Die vom Entwickler gewählten Verben können einfach abhängig von seiner Laune gesetzt werden. Einige Leute bevorzugen beispielsweise die Option "get" und andere bevorzugen das Abrufen. Daher ist es besser, sich auf das bekannte HTTP-GET-Verb zu beschränken, das Ihnen genau sagt, was der Endpunkt tut.



Die Aktion muss im Namen der HTTP-Methode der von uns gestellten Anforderung angegeben werden. Die gebräuchlichsten Methoden enthalten die Verben GET, POST, PUT und DELETE.

GET holt Ressourcen. POST sendet neue Daten an den Server. PUT aktualisiert vorhandene Daten. DELETE löscht Daten. Jedes dieser Verben entspricht einer der Operationen aus der CRUD- Gruppe .



In Anbetracht der beiden oben diskutierten Prinzipien müssen wir Routen des Formulars GET erstellen, um neue Artikel zu erhalten /articles/. In ähnlicher Weise verwenden wir POST /articles/, um einen neuen Artikel /articles/:id zu aktualisieren , PUT , um einen Artikel mit dem angegebenen zu aktualisieren id. Die DELETE-Methode dient /articles/:idzum Löschen eines Artikels mit einer bestimmten ID.



/articlesIst eine REST-API-Ressource. Mit Express können Sie beispielsweise Folgendes mit Artikeln tun:



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'));


Im obigen Code haben wir Endpunkte für die Bearbeitung von Artikeln definiert. Wie Sie sehen können, enthält der Pfadname keine Verben. Nur Namen. Verben werden nur in den Namen von HTTP-Methoden verwendet.



Die POST-, PUT- und DELETE-Endpunkte akzeptieren einen JSON-Anforderungshauptteil und geben eine JSON-Antwort zurück, einschließlich eines GET-Endpunkts.



Sammlungen werden Pluralnomen genannt



Sammlungen sollten mit mehreren Substantiven benannt werden. Es kommt nicht oft vor, dass wir nur ein Element aus einer Sammlung entnehmen müssen. Daher müssen wir konsistent sein und mehrere Substantive in Sammlungsnamen verwenden.



Der Plural wird auch aus Gründen der Konsistenz mit Namenskonventionen in Datenbanken verwendet. In der Regel enthält eine Tabelle nicht einen, sondern viele Datensätze, und die Tabelle wird entsprechend benannt.



Wenn Sie mit einem Endpunkt arbeiten, verwenden /articleswir Plural, wenn Sie alle Endpunkte benennen.



Verschachteln von Ressourcen beim Arbeiten mit hierarchischen Objekten



Der Pfad der Endpunkte, die sich mit verschachtelten Ressourcen befassen, sollte folgendermaßen strukturiert sein: Fügen Sie die verschachtelte Ressource als Pfadnamen nach dem Namen der übergeordneten Ressource hinzu.

Sie müssen sicherstellen, dass die Verschachtelung von Ressourcen im Code genau der Verschachtelung von Informationen in unseren Datenbanktabellen entspricht. Andernfalls ist Verwirrung möglich.



Wenn wir beispielsweise an einem bestimmten Endpunkt Kommentare zu einem neuen Artikel erhalten möchten, müssen wir den Pfad / die Kommentare an das Ende des Pfads anhängen /articles. In diesem Fall wird davon ausgegangen, dass wir die Kommentarentität als untergeordnete Entität articlein unserer Datenbank betrachten.



Sie können dies beispielsweise mit dem folgenden Code in Express tun:



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'));


Im obigen Code können Sie die GET-Methode für den Pfad verwenden '/articles/:articleId/comments'. Wir erhalten Kommentare commentszu dem passenden Artikel articleIdund senden ihn als Antwort zurück. Wir fügen 'comments'nach dem Pfadsegment hinzu '/articles/:articleId', dass dies eine untergeordnete Ressource ist /articles.



Dies ist sinnvoll, da Kommentare untergeordnete Objekte sind articlesund davon ausgegangen wird, dass jeder Artikel seine eigenen Kommentare enthält. Andernfalls kann diese Struktur für den Benutzer verwirrend sein, da sie normalerweise für den Zugriff auf untergeordnete Objekte verwendet wird. Das gleiche Prinzip gilt für die Arbeit mit POST-, PUT- und DELETE-Endpunkten. Sie alle verwenden beim Erstellen von Pfadnamen dieselbe Strukturverschachtelung.



Ordentliche Fehlerbehandlung und Rückgabe von Standardfehlercodes



Um Verwirrung zu vermeiden, wenn ein Fehler in der API auftritt, behandeln Sie Fehler sorgfältig und geben Sie HTTP-Antwortcodes zurück, die angeben, welcher Fehler aufgetreten ist. Dadurch erhalten API-Betreuer ausreichende Informationen, um das Problem zu verstehen. Es ist nicht akzeptabel, dass Fehler das System zum Absturz bringen. Daher können sie nicht ohne Verarbeitung belassen werden, und der API-Verbraucher muss sich mit dieser Verarbeitung befassen.



Die häufigsten HTTP-Fehlercodes sind:



  • 400 Bad Request - Zeigt an, dass die vom Client empfangene Eingabe die Validierung fehlgeschlagen ist.
  • 401 Nicht autorisiert - bedeutet, dass sich der Benutzer nicht angemeldet hat und daher keine Berechtigung zum Zugriff auf die Ressource hat. Normalerweise wird dieser Code ausgegeben, wenn der Benutzer nicht authentifiziert ist.
  • 403 Verboten - Zeigt an, dass der Benutzer authentifiziert ist, aber keine Berechtigung zum Zugriff auf die Ressource hat.
  • 404 Nicht gefunden - bedeutet, dass die Ressource nicht gefunden wurde
  • Der 500 Internal Server Error ist ein Serverfehler und sollte wahrscheinlich nicht explizit ausgelöst werden.
  • 502 Bad Gateway - Zeigt eine ungültige Antwortnachricht vom Upstream-Server an.
  • 503 Dienst nicht verfügbar - bedeutet, dass auf der Serverseite etwas Unerwartetes passiert ist - z. B. Serverüberlastung, Ausfall einiger Systemelemente usw.


Sie sollten genau die Codes ausgeben, die dem Fehler entsprechen, der unsere Anwendung verhindert hat. Wenn wir beispielsweise Daten ablehnen möchten, die als Anforderungsnutzdaten empfangen wurden, müssen wir gemäß den Regeln der Express-API einen Code von 400 zurückgeben:



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'));


Im obigen Code halten wir im Benutzerarray eine Liste der vorhandenen Benutzer, denen E-Mails bekannt sind.



Wenn wir versuchen, eine Nutzlast mit einem Wert zu übertragen, der emailbereits in Benutzern vorhanden ist, erhalten wir eine Antwort mit einem Code von 400 und einer Nachricht, die 'User already exists'angibt, dass ein solcher Benutzer bereits vorhanden ist. Mit diesen Informationen kann der Benutzer besser werden - ersetzen Sie die E-Mail-Adresse durch die, die noch nicht auf der Liste steht.



Fehlercodes sollten immer von Meldungen begleitet werden, die informativ genug sind, um den Fehler zu beheben, aber nicht so detailliert, dass diese Informationen von Angreifern verwendet werden können, die beabsichtigen, unsere Informationen zu stehlen oder das System zum Absturz zu bringen.



Wenn unsere API nicht ordnungsgemäß heruntergefahren werden kann, müssen wir den Fehler sorgfältig behandeln, indem wir Fehlerinformationen senden, damit der Benutzer die Situation leichter korrigieren kann.



Ermöglichen das Sortieren, Filtern und Paginieren von Daten



Die Grundlagen hinter der REST-API können stark wachsen. Manchmal gibt es so viele Daten, dass es unmöglich ist, alle Daten auf einmal zurückzugewinnen, da dies das System verlangsamt oder sogar herunterfährt. Daher brauchen wir eine Möglichkeit, Elemente zu filtern.



Wir brauchen auch Möglichkeiten, die Daten zu paginieren (Paginierung), damit wir jeweils nur wenige Ergebnisse zurückgeben. Wir möchten nicht zu lange mit Ressourcen arbeiten, um alle angeforderten Daten gleichzeitig abzurufen.



Sowohl das Filtern als auch die Datenpaginierung können die Leistung verbessern, indem der Einsatz von Serverressourcen reduziert wird. Je mehr Daten sich in der Datenbank ansammeln, desto wichtiger werden diese beiden Möglichkeiten.



Hier ist ein kleines Beispiel, in dem die API eine Abfragezeichenfolge mit verschiedenen Parametern akzeptieren kann. Filtern wir die Elemente nach ihren Feldern:



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'));


Im obigen Code haben wir eine Variable req.query, mit der wir Anforderungsparameter abrufen können. Wir können dann Eigenschaftswerte extrahieren, indem wir einzelne Abfrageparameter in Variablen destrukturieren. JavaScript hat hierfür eine spezielle Syntax.



Schließlich wenden wir Filter auf jeden Abfrageparameterwert an, um die Elemente zu finden, die wir zurückgeben möchten.



Nachdem dies erledigt ist, geben wir die Ergebnisse als Antwort zurück. Wenn Sie also eine GET-Anforderung an den folgenden Pfad mit einer Abfragezeichenfolge senden:



/employees?lastName=Smith&age=30


Wir bekommen:



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


als zurückgegebene Antwort, da die Filterung aktiviert war lastNameund age.



Ebenso können Sie den Seitenabfrageparameter akzeptieren und eine Gruppe von Datensätzen zurückgeben, die Positionen von (page - 1) * 20bis belegen page * 20.



Außerdem können Sie in der Abfragezeichenfolge die Felder angeben, nach denen sortiert werden soll. In diesem Fall können wir sie nach diesen separaten Feldern sortieren. Beispielsweise müssen wir möglicherweise eine Abfragezeichenfolge aus einer URL wie der folgenden extrahieren:



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


Wo +bedeutet "hoch" und "runter". Wir sortieren also alphabetisch nach Autorennamen und nach Datum, das vom neuesten zum ältesten veröffentlicht wurde.



Befolgen Sie die bewährten Sicherheitspraktiken



Die Kommunikation zwischen Client und Server sollte größtenteils privat sein, da wir häufig vertrauliche Informationen senden und empfangen. Daher ist die Verwendung von SSL / TLS aus Sicherheitsgründen ein Muss.



Das Hochladen des SSL-Zertifikats auf den Server ist nicht so schwierig, und das Zertifikat selbst ist entweder kostenlos oder sehr günstig. Es gibt keinen Grund aufzugeben, unsere REST-APIs nicht über offene, sondern über sichere Kanäle kommunizieren zu lassen.



Eine Person sollte nicht Zugang zu mehr Informationen erhalten, als sie angefordert hat. Beispielsweise sollte ein normaler Benutzer keinen Zugriff auf die Informationen eines anderen Benutzers erhalten. Außerdem sollte er die Daten von Administratoren nicht anzeigen können.



Um das Prinzip der geringsten Berechtigungen zu fördern, müssen Sie entweder die Rollenprüfung für eine bestimmte Rolle implementieren oder für jeden Benutzer eine genauere Rollenverteilung bereitstellen.



Wenn wir Benutzer in mehrere Rollen gruppieren möchten, müssen die Rollen mit solchen Zugriffsrechten versehen werden, die sicherstellen, dass alles, was der Benutzer benötigt, erledigt wird und nicht mehr. Wenn wir die Zugriffsrechte für jede dem Benutzer bereitgestellte Opportunity detaillierter vorschreiben, müssen wir sicherstellen, dass der Administrator diese Funktionen jedem Benutzer gewähren oder diese Funktionen wegnehmen kann. Darüber hinaus müssen Sie einige vordefinierte Rollen hinzufügen, die auf eine Benutzergruppe angewendet werden können, damit Sie nicht die erforderlichen Rechte für jeden Benutzer manuell festlegen müssen.



Cache-Daten zur Verbesserung der Leistung



Caching kann hinzugefügt werden, um Daten aus einem lokalen Speichercache zurückzugeben, anstatt einige Daten aus der Datenbank abzurufen, wenn Benutzer dies anfordern. Der Vorteil des Caching besteht darin, dass Benutzer Daten schneller abrufen können. Diese Daten sind jedoch möglicherweise veraltet. Dies kann auch mit Problemen beim Debuggen in Produktionsumgebungen behaftet sein, wenn ein Fehler aufgetreten ist und wir weiterhin die alten Daten untersuchen.



Es stehen verschiedene Caching-Optionen zur Verfügung, z. B. Redis , In-Memory-Caching und mehr. Sie können die Art und Weise, wie Daten zwischengespeichert werden, nach Bedarf ändern.



Zum Beispiel bietet Express Middleware anapicacheHinzufügen von Caching-Funktionen zu Ihrer Anwendung ohne komplizierte Konfiguration. Einfaches In-Memory-Caching kann dem Server wie folgt hinzugefügt werden:



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'));


Der obige Code bezieht sich einfach auf apicachemit apicache.middleware, was zu Folgendem führt:



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


und das reicht aus, um anwendungsweites Caching anzuwenden. Wir speichern zum Beispiel alle Ergebnisse in fünf Minuten zwischen. Anschließend kann dieser Wert je nach Bedarf angepasst werden.



API-Versionierung



Wir benötigen unterschiedliche Versionen der API, falls wir Änderungen daran vornehmen, die den Client stören könnten. Die Versionierung kann semantisch erfolgen (z. B. bedeutet 2.0.6, dass die Hauptversion 2 ist und dies der sechste Patch ist). Dieses Prinzip wird heute in den meisten Anwendungen akzeptiert.



Auf diese Weise können Sie alte Endpunkte schrittweise außer Betrieb setzen, anstatt alle zu zwingen, gleichzeitig zur neuen API zu wechseln. Sie können die v1-Version für diejenigen behalten, die nichts ändern möchten, und die v2-Version mit all ihren neuen Funktionen für diejenigen bereitstellen, die bereit sind, ein Upgrade durchzuführen. Dies ist besonders wichtig im Zusammenhang mit öffentlichen APIs. Sie müssen versioniert werden, um Anwendungen von Drittanbietern, die unsere APIs verwenden, nicht zu beschädigen.



Die Versionierung erfolgt normalerweise durch Hinzufügen von /v1/,/v2/usw. am Anfang des API-Pfads hinzugefügt.



So geht es beispielsweise in 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'));


Wir fügen einfach die Versionsnummer am Anfang des Pfades hinzu, der zum Endpunkt führt.



Fazit



Der wichtigste Aspekt beim Entwurf hochwertiger REST-APIs ist die Wahrung der Konsistenz durch Befolgung der Standards und Konventionen des Webs. JSON-, SSL / TLS- und HTTP-Statuscodes sind heute ein Muss im Web.



Leistung ist ebenso wichtig. Sie können es erhöhen, ohne zu viele Daten gleichzeitig zurückzugeben. Darüber hinaus können Sie das Caching verwenden, um zu vermeiden, dass Sie immer wieder nach denselben Daten fragen.



Endpunktpfade müssen konsistent benannt werden. Sie sollten Substantive in ihren Namen verwenden, da Verben in den Namen von HTTP-Methoden vorhanden sind. Verschachtelte Ressourcenpfade müssen dem übergeordneten Ressourcenpfad folgen. Sie sollten mitteilen, was wir erhalten oder manipulieren, damit wir nicht zusätzlich die Dokumentation konsultieren müssen, um zu verstehen, was passiert.



All Articles