Pragmatisch, praktisch gut: RESTful APIs

RESTful-API-Design: Eine Einführung
1 Kommentar

Mehr als fünfzehn Jahre nach seiner „Erfindung“ durch Roy Fielding scheint RESTful-API-Design aktueller denn je. Egal ob im Umfeld von Microservices oder bei der Öffnung bisher nur intern genutzter Systeme – REST wohin man schaut! Aber was genau ist eigentlich REST? Nur weil JSON oder XML Payload via HTTP an einen Server gesendet beziehungsweise von eben diesem empfangen wird, hat man es noch lange nicht mit einem RESTful API zu tun. Was genau zeichnet also eine REST-Schnittstelle aus? Und ab wann kann man sie als wirklich gelungen bezeichnen? Spätestens bei dieser Frage scheiden sich die Geister.

Auf den ersten Blick scheint das Design eines RESTful API denkbar einfach. „One noun, four verbs“, so einfach kann Programmierung sein. Also kurz eine Ressource identifiziert, zusehen, dass man deren Manipulation (CRUD-Operationen) mittels Endpoint und HTTP-Methoden (POST, GET, PUT, DELETE) ermöglicht – und fertig ist das REST-API. Stellen wir uns einmal exemplarisch eine Schnittstelle vor, über die Bestellungen aufgegeben, abgefragt, geändert und gelöscht werden können (Listing 1).

// Create a new order via http POST and JSON payload
// representing the order to create. 
POST /api/orders 
[ ... payload representing an order ...]

// Update an existing order with id 123 via http PUT 
// and JSON payload representing the changed order. 
PUT /api/orders/123 
[ ... payload representing the changed order ...]

// Delete an existing order with id 123 via http DELETE. 
DELETE /api/orders/123 

// Retrieve an existing order with id 123 via http GET. 
GET /api/orders/123 

// Retrieve all existing orders http GET. 
GET /api/orders

Ist das wirklich schon alles? Passt die 1:1-Zurordnung von CRUD-Operationen und HTTP-Methoden tatsächlich immer? Und wie sehen eigentlich parametrisierte Ressourcenabfragen (alias Filter) zur gezielten Einschränkung der Treffermenge aus? Wie individualisiere ich das Rückgabeformat, z. B. für die Verwendung auf mobilen Endgeräten? Wie wird Security gehandhabt? Was passiert im Falle eines Fehlers? Und wie stelle ich eine mögliche Evolution des API sicher? Fragen über Fragen, aber keine Angst. Wir werden uns Schritt für Schritt mit deren Beantwortung beschäftigen.

Aller Anfang ist schwer

Das API ist das UI des Entwicklers. Ähnlich wie bei einem guten UX-/UI-Design sollte also auch beim Design eines API darauf geachtet werden, dass die Erwartungen des Anwenders – in diesem Fall also des Entwicklers – erfüllt werden. Wie aber genau sehen diese Erwartungen aus? Die gute Nachricht: Im Umfeld von REST gibt es etliche Spezifikationen, an denen man sich orientieren kann und sollte. Dies gilt zum Beispiel für die korrekte Verwendung der HTTP-Methoden und des HTTP-Statuscodes. Und findet man für eine Problemstellung keine Spezifikation, dann kann es nicht schaden, einen Blick auf bekannte und stark frequentierte APIs (Facebook, Amazon, Twitter, GIT und Co.) zu werfen und ein wenig abzugucken. In der Regel finden sich dort Patterns und Best Practices wieder, die sich im Laufe der letzten Jahre in der REST-Community etabliert haben.

POST vs. PUT vs. PATCH

Wie oben gezeigt, liegt eine 1:1-Zuordnung von CRUD-Operationen und HTTP-Methoden nahe. Dies ist auch in den meisten Fällen korrekt. Allerdings gilt es gemäß HTTP-1.1-Spezifikation einige Feinheiten zu beachten. Dort heißt es u. a., dass POST eine Kindressource an einem vom Server definierten URL anlegt. Anders formuliert erzeugt der Server bei einem POST eine neue Ressource und vergibt für sie eine eindeutige ID, die in der Regel als Bestandteil des zur neuen Ressource gehörenden URLs mittels Location-Header an den aufrufenden Client zurückgegeben wird (Listing 2).

// POST request to create new order 
POST /api/orders 
[ ... payload representing an order ...]

// Response with success code and location header 
HTTP/1.1. 201 Created 
  Location: /api/orders/

Was aber ist, wenn die ID und somit der zur Identifikation der Ressource zu verwendende URL vom Client vorgegeben werden soll? Hier kommt die HTTP-Methode PUT ins Spiel. Laut Spezifikation ersetzt oder erzeugt ein HTTP-PUT eine Ressource an einem clientdefinierten URL. Der folgende Aufruf überschreibt eine existierende Bestellung mit der ID 123, soweit vorhanden, oder legt sie alternativ mit der durch den Client vorgegebenen ID neu an (Listing 3).

// PUT request to replace existing 
PUT /api/orders/123 
[ ... payload representing an order ...]

// Response with success code and location header 
// 200 OK, if existing order 123 replaced
// 201 Created, if new order 123 created
HTTP/1.1. 200 OK oder 201 Created 
  Location: /api/orders/

Die HTTP-Methode PUT kann also mehrere Aufgaben wahrnehmen. Ob die clientseitige Vergabe einer ID für das eigene API erlaubt sein soll und ein PUT entsprechend implementiert werden muss, ist Frage des API-Designs. Neben PUT existiert noch eine weitere, weniger bekannte HTTP-Methode zur Veränderung von Ressourcen: PATCH. Während ein PUT zu einer vollständigen Ersetzung der Ressource führt und somit die Payload auch immer die gesamte Ressource enthalten muss, kommt es bei einem PATCH lediglich zu einem Update gemäß der innerhalb der Payload angegebenen Änderungen. Möchte man zum Beispiel bei einer Bestellung das Feld „Bemerkung“ ändern, müsste bei einem PUT die gesamte Bestellung inklusive des geänderten Felds „Bemerkung“ in der Payload übertragen werden. Bei einem PATCH reicht hingegen lediglich das geänderte Feld bzw. eine entsprechende Beschreibung der Änderung als Payload aus. Was auf den ersten Blick recht attraktiv klingt, ist in der Praxis nicht ganz trivial zu realisieren. Je nach Payload-Format kann das Auswerten der Änderungswünsche und das anschließende Ausführen recht komplex werden.

Safe und idempotent

Die HTTP-1.1-Spezifkation hat noch weitere Besonderheiten zu bieten, die beim Design eines RESTFul API berücksichtigt werden sollten. So charakterisiert sie u. a. einige ihrer Methoden als safe (GET, HEAD und OPTIONS) und/oder idempotent (GET, HEAD, OPTIONS, PUT und DELETE). safe-Methoden dürfen eine Ressource bzw. deren Repräsentation nicht verändern. Anders formuliert: Ein lesender Zugriff auf eine Ressource via HTTP GET sollte niemals einen schreibenden Seiteneffekt auslösen. Die folgenden Aufrufe – à la RPC – zur Manipulation einer Bestellung sind also tabu:

  • GET /api/orders?add=item …
  • GET /api/orders/1234/addItem …

Was aber, wenn zum Beispiel ein GET die Ressource zurückliefert und gleichzeitig ein lastAccessed-Feld in der Datenbank aktualisiert? Solange diese Manipulation nur einen Einfluss auf die Repräsentation innerhalb der serverseitigen Domäne oder Datanbank hat, ist sie erlaubt. Wird das Feld dagegen mit über die Schnittstelle nach außen gegeben – die Frage nach dem Sinn blenden wir hier einmal aus – dann ist die Methode nicht mehr safe und somit die Erwartung des Anwenders nicht erfüllt. Zugegeben, das obige Beispiel wirkt ein wenig konstruiert. Etwas paxisnäher wird es da schon bei der Betrachtung der idempotent-Methoden. Hier gilt die Regel, dass eine mehrfache Anwendung der Methode nur zu einer einmaligen Änderung an der Ressourcenrepräsentation führen darf. Ein mehrfacher Aufruf von PUT darf die Ressource bzw. deren Repräsentation also nur einmalig ändern, ein mehrfacher Aufruf von DELETE die Ressource nur einmalig löschen.

Nur einmalig ändern und nur einmalig löschen? Ist das nicht selbstverständlich? Nicht unbedingt. Würde man ein PUT z. B. so implementieren, dass es ein neues Produkt zu einer bestehenden Bestellung hinzufügt, dann würde ein mehrfacher Aufruf entsprechend auch mehrere Produkte hinzufügen. Und genau das ist untersagt. Die Payload des PUT ersetzt also laut Spezifikation die bisherige Repräsentation der Ressource vollständig. Einmal ersetzt, führen weitere Aufrufe zu keiner weiteren Änderung und liefern auch stets denselben HTTP-Statuscode wie beim ersten Aufruf zurück (200 für OK oder 204 für no content).

So weit, so gut. Aber was bedeutet idempotent im Zusammenhang mit DELETE? Sollte auch hier ein mehrfacher Aufruf immer denselben HTTP-Statuscode zurückgeben? Nicht zwingendermaßen. idempotent bedeutet lediglich, dass sich der Status der Ressource auf dem Server bei mehrfachen Aufrufen nur einmalig ändern darf und die erneuten Aufrufe keine weiteren Seiteneffekte auslösen sollten. idempotent macht dagegen keinerlei Aussagen über den Rückgabewert. Ein zweiter Löschversuch kann also durchaus einen anderen HTTP-Statuscode zurückgeben (z. B. 404 für not found) als der erste, um so dem Client zu signalisieren, dass der Aufruf so keinen Sinn macht.

Die oben erwähnte PATCH-Methode ist übrigens weder safe noch idempotent. Das liegt daran, dass ihre Payload frei definierbar ist. Nicht nur einzelne Felder der zu ändernden Ressource sind erlaubt, sondern zum Beispiel auch Änderungsanweisungen. Ein Aufruf von PATCH mit der in Listing 3 gezeigten JSON-Struktur gemäß RFC 6902 würde zum Beispiel das Item mit der ID 123 in der Bestellung überschreiben und ein zweites zur Bestellung hinzufügen. Ein mehrfacher Aufruf hätte entsprechend das mehrfache Hinzufügen des Items zur Bestellung zur Folge:

[
  {"op": "add", "path":"/items", "value": [ ...  some item data ... ]},
  {"op": "replace", "path":"/items/123", "value": [ ...  item 123 data ... ]}
]

Filter und Co.

Einzelne Ressourcen oder alle Ressourcen eines Typs abzufragen, ist in REST per Definition denkbar einfach. Was aber, wenn man gezielt einen Filter oder ein Limit für eine Abfrage setzen möchte? Schließlich möchte man als Client ja nicht unbedingt mit einer Abfrage die gesamte Datenbank über die Leitung laden. Und selbst wenn dies die Intention des Clients ist, sollte der Server dieses unterbinden und die Treffermenge künstlich begrenzen.

Auch wenn es für das Filtern von Abfragen keine konkrete Spezifikation gibt, haben sich in der REST-Community einige Best Practices etabliert. In der einfachsten Variante gibt man die zu filternden Felder sowie ihre Werte als Query-Parameter an. Die in Listing 4 gezeigten Abfragen würden zum Beispiel alle offenen Bestellungen, alle Bestellungen über 100 Euro oder die ersten zehn Bestellungen liefern.

// Ask for all open orders
GET /api/orders?state=open

// Ask for all orders more expensive
// than 100.00 Euro
GET /api/orders?state=open

// Ask for the first 10 orders
GET /api/orders?state=open

Das Ganze ließe sich natürlich durch Konkatenation der einzelnen Filter beliebig kombinieren. Auch eine Sortierung der Ergebnismenge ist durch Angabe eines spezifischen Query-Parameters (z. B. sort) sowie der gewünschten Sortierung (z. B. +/- oder asc/desc möglich): GET /api/orders?sort=-status,+price.

Möchte man als Ergebnis nicht alle Felder der Bestellung erhalten, lässt sich auch dies durch einen weiteren Query-Parameter (z. B. fields für einzubindende Felder oder exclude für zu ignorierende Felder ) realisieren:

  • GET /api/orders?fields=id,status,price,date
  • GET /api/orders?exclude=id,date

Was im Einzelnen noch einfach aussieht, kann in Kombination schnell so komplex werden, dass es kaum noch zu handhaben ist. Stellen wir uns zum Beispiel vor, dass wir sowohl ein „und“ als auch ein „oder“ bei der kombinierten Abfrage erlauben wollen. Und stellen wir uns weiterhin vor, dass wir auch Gruppen von Filtern und deren Verknüpfung beliebig via „und“ oder „oder“ kombinieren können wollen und dabei die Rückgabemenge der Felder für mobile Endgeräte einschränken möchten. Wie würde zum Beispiel der URL für „alle Bestellungen a) der letzten zwei Tage UND mit einem Warenwert über 100 Euro ODER dem Status offen ODER in Bearbeitung ODER b) mit einem Warenwert unter 100 Euro UND dem heutigen Bestelldatum UND dem Status offen, sortiert nach DATUM, STATUS und WARENWERT“ aufbereitet für die Mobile-Version aussehen? Bei derart komplexen Abfragen ist man schnell versucht, eine eigene Query Language – inklusive Parser – und somit das Rad neu zu erfinden. Erste Abhilfe können hier, zumindest für Standardabfragen, Aliasse sowohl für vordefinierte Filter- als auch Feldkombinationen schaffen. Diese sind dann als eine Art virtuelle Ressource zu verstehen. Alternativ kann die gewünschte Einschränkung der Felder auch via HTTP-Header (Prefer) signalisiert werden (Listing 5).

// Use virtual resource open_orders as 
// an alias for /api/orders?status=open 
GET /api/open_orders 

// Use style=mobile parameter as an alias 
// for a predefined set of field filters 
GET /api/orders?style=mobile 

// Use Prefere-Header return=mobile-format
// as an alias for a predefined set of field filters
// in combination with open_orders alias
GET /api/open_orders
Prefer: return=mobile-format

Eine deutlich flexiblere Alternative stellt die an FIQL (Feed Item Query Language) angelehnte RQL (Resource Query Language) dar. Mithilfe dieser Object-style Query Language und den zugehörigen Parsern lassen sich beliebige Abfragen auf bestehenden Ressourcen abbilden und direkt auf JS-Arrays, SQL, MongoDB oder Elasticsearch absetzen. Dank Java-Parser und JPA Criteria Builder ist auch eine einfache Überführung in eine JPA Query möglich. Für komplexe Abfragen inklusive Filter auf einzelnen Ressourcen ist RQL somit eine optimale Wahl.

Aber auch diese Variante stößt irgendwann an ihre Grenze. Nämlich immer dann, wenn eine Abfrage nicht nur auf einer Ressource, sondern als eine Art Join auf mehreren Ressourcen stattfinden soll. In der Regel führt dieses Problem dazu, dass vom Client die Abfragen in mehreren Roundtrips abgesetzt werden. Möchten wir zum Beispiel alle Bestellungen der letzten Woche aus allen Filialen erfragen, die in einem bestimmten PLZ-Gebiet liegen, dann würden wir zunächst die passenden Filialen abfragen und dann je Filiale deren Bestellungen. Das Zusammenführen der Ergebnisse würde entsprechend auf dem Client passieren – das klassische N+1-Dilemma. Spätestens jetzt sind wir an einem Punkt angelangt, an dem wir uns nach einer gangbaren Alternative – jenseits von REST – umschauen sollten. GraphQL aus dem Hause Facebook ist hier mehr als nur einen Blick wert! Mithilfe von GraphQL können beliebige Abfragen auf einem Objektgraphen abgesetzt und die gewünschten Ergebnisfelder der involvierten Ressourcen gezielt kombiniert und abgefragt werden. Unabhängig von der Anzahl der Ressourcen kann das Ergebnis so mit nur einem einzigen Roundtrip erfragt werden.

Pagination

Fragt ein Client eine potenziell große Menge an Ressourcen an, zum Beispiel alle Bestellungen, dann sollte die Anfrage von vornherein eingeschränkt werden. In der Regel wird dazu Pagination (Paginierung), also die Angabe einer Seite, verwendet. Generell kann dies auf zwei unterschiedlichen Wegen passieren (Listing 6). In der ersten Variante gibt der Client innerhalb des Requests eine konkrete Seitennummer, z. B. als Query-Parameter, mit. Dies impliziert, dass der Server festlegt, wie viele Treffer pro Seite existieren sollen, und das Offset entsprechend berechnet. In der zweiten Variante übergibt der Client sowohl Offset als auch die maximale Anzahl der zu übertragenden Ressourcen. Dies erfordert zwar ein wenig mehr Intelligenz auf dem Client, bringt aber im Gegenzug deutlich mehr Flexibilität mit sich.

// Pagination variant 1: 
// Use concrete page numbers, calculate
// offset and limit on client side 
GET /api/open_orders?page=3 

// Pagination variant 2: 
// Calculate offset and limit on client
// side and use values as query parameter 
GET /api/orders?offset=30,limit=10

Pagination ist immer dann interessant, wenn durch größere Datenmengen navigiert werden soll. Stellen wir uns zum Beispiel eine Tabelle innerhalb einer Single Page Application (SPA) vor, innerhalb derer wir vor- und zurückblättern können. Auch ein Sprung zur ersten und letzten Seite der Tabelle soll möglich sein. Um die Links der entsprechenden Navigationsbuttons richtig zu berechnen, benötigt der Client einiges an Informationen. Wäre es da nicht schön, wenn ihm diese Arbeit vom Server abgenommen werden könnte? Kein Problem. Zu diesem Zweck muss der Server lediglich entsprechende Linkreferenzen in der Response mitschicken. Diese können entweder als Teil der Payload generiert werden oder alternativ als Linkheader (Listing 7).

// get "page 3" and info about PREV/NEXT 
GET /api/orders?offset=20&limit=10 HTTP/1.1

// Response with success code and link header for 
// navigation purpose
HTTP/1.1. 206 Partial Content
  Link: <.../api/orders?offset=0&limit=10>; rel="first"
        <.../api//orders?offset=10&limit=10>; rel="prev",
        <.../api//orders?offset=30&limit=10>; rel="next",
        <.../api//orders?offset=40&limit=3>; rel="last"

Header

Wie bereits an mehreren Beispielen gezeigt, spielen HTTP-Header im Umfeld von REST eine ganz besondere Rolle. Doch was gehört in den Body und was in den Header? Und wann sollte ein Path- oder Query-Parameter innerhalb des URLs und wann ein Header genutzt werden?

Allgemein kann man sagen, dass der Header eher für globale Metadaten und der Body für business- bzw. Request-spezifische Informationen verwendet werden sollte. Gleiches gilt für die Parameter. Während Path- oder Query-Parameter für ressourcenspezifische Parameter genutzt werden sollten, dient der HTTP-Header zum Austausch allgemeiner Metadaten. So findet sich im Path-Parameter zum Beispiel die Angabe einer Subressource oder einer Resource-ID und im Query-Parameter ein spezifischer Filter inklusive Filterwert. Im Header dagegen werden Angaben zum Austauschformat (Accept, Content-Type), zur Security (Authorization-Header) oder – wie bereits gesehen – zu möglichen weiteren Aktionen (Link-Header) gemacht. Die Verwendung von HTTP-Headern bietet übrigens den großen Vorteil, dass auf die Headerwerte gezielt zugegriffen werden kann, ohne dass dazu die gesamte Payload geparst werden muss.

Natürlich ist neben der Verwendung von standardisierten Headern auch die Angabe von Custom-Headern möglich, um selbstdefinierte Metadaten der eigenen Schnittstelle auszutauschen. In der REST-Community wird für derartige Custom-Header meist ein „X-“ als Präfix verwendet, um so zu signalisieren, dass es sich nicht um einen standardisierten Header handelt. Allerdings gilt die Verwendung des Präfixes „X-“ seit 2012 als deprecated. Grund hierfür ist, dass eine spätere Überführung des individuellen Headers in einen Standard, und damit verbunden das Entfernen des „X-“-Präfixes, die Abwärtskompatibilität brechen würde. Ein gutes Beispiel dafür ist der GZIP-Header, der derzeit von Clients und Servern sowohl in der Form x-gzip als auch gzip unterstützt werden muss. Nun kann sich jeder RESTful-API-Designer die Frage stellen, wie wahrscheinlich es ist, dass ein proprietärer Header des eigenen API es jemals in einen offenen Standard schafft, und pragmatisch abwägen, das „X-“-Präfix zu verwenden oder eben nicht.

Statuscodes

Mindestens ebenso wichtig und hilfreich, wie die korrekte Verwendung der HTTP-Methoden und -Header, ist die gezielte Anwendung der zur Verfügung stehenden HTTP-Statuscodes. Es kommt nicht von ungefähr, dass neben den drei am häufigsten anzutreffenden Codes 200 (OK), 400 (Bad Request) und 500 (Internal Server Error) noch eine ganze Liste weiterer sinnvoller Codes existiert.

Zunächst einmal sollte darauf geachtet werden, dass der Server einen Code des richtigen Nummernkreises zurückgibt. Während die Gruppe der 100er-Statuscodes (Informationen) anzeigen, dass die Bearbeitung der Anfrage noch andauert, signalisieren die 200er (erfolgreiche Operationen) eine korrekte Abarbeitung des Requests. Die Gruppe der 300er-Statuscodes (Umleitung) wiederum zeigen dem Client auf, dass zur erfolgreichen Bearbeitung des Requests noch weitere Schritte des Clients notwendig sind. Durch die Gruppe der 400er-Statuscodes (Clientfehler) werden Probleme des Requests aufgezeigt, während die 500er (Serverfehler) signalisieren, dass der Server nicht in der Lage ist, die Anfrage sinnvoll zu bearbeiten. Durch die Wahl der richtigen Nummerngruppe wird dem Client also unter anderem aufgezeigt, ob eine Wiederholung des Requests – evtl. mit geänderten Anfragedaten – Sinn ergibt oder nicht.

Angular Kickstart: von 0 auf 100

mit Christian Liebel (Thinktecture AG) und Peter Müller (Freelancer)

JavaScript für Softwareentwickler – für Einsteiger und Umsteiger

mit Yara Mayer (evia) und Sebastian Springer (MaibornWolff)

API Conference 2018

API Management – was braucht man um erfolgreich zu sein?

mit Andre Karalus und Carsten Sensler (ArtOfArc)

Web APIs mit Node.js entwickeln

mit Sebastian Springer (MaibornWolff GmbH)

Auch bei der Verwendung der Statuscodes sollte wieder die Erwartungshaltung des API-Anwenders im Fokus stehen. Fragt dieser zum Beispiel die Liste aller offenen Bestellungen ab und bekommt als Antwort eine leere Liste und den Statuscode 200, kann er sich nicht sicher sein, ob die Payload bewusst leer ist oder auf Serverseite ein Fehler vorliegt. Anders dagegen sieht es aus, wenn der Server eine leere Liste und den Code 204 (No Content) liefert. In diesem Fall wird dem API-Anwender bewusst jeglicher Interpretationsspielraum – sprich potenzielle Fehlerquellen – genommen. Gleiches gilt, wenn der API-Anwender via Pagination lediglich eine limitierte Teilmenge anfragt. In diesem Fall sollte der Server anstelle von 200 den Code 206 (Partial Content) liefern und so signalisieren, dass aufseiten des Servers noch weitere Treffer warten und deren Verlinkung sich im Heaeder bzw. der Payload findet. Legt der API-Anwender eine neue Ressource an, sollte der Server dies mit 201 (Created) statt nur 200 quittieren. Auch hier hat der spezifische Code eine deutlich stärkere Aussagekraft als der allgemeine. Kann der Server die Anfrage nicht direkt, also synchron, bearbeiten, sondern lediglich deren Bearbeitung asynchron anstoßen, sollte dies durch den Code 202 (Accepted) signalisiert werden. So weiß der Client, dass es aufseiten des Servers noch nicht zwingend zu einer Änderung an der Ressource gekommen ist, diese aber auf jeden Fall stattfinden wird.

Neben den bisher aufgezeigten Codes aus dem Bereich 200 sollten natürlich auch die verschiedenen Codes der anderen Bereiche gezielt genutzt werden. So hat ein 401 (Unauthorized) oder 403 (Forbidden) sicherlich eine ganz andere Aussagekraft als der eher generische Fehlercode 400. Gleiches gilt für 404 (Not Found), 405 (Method Not Allowed) und 429 (Too Many Requests).
Eine wirklich gute interaktive Übersicht der einzelnen HTTP-Statuscodes inklusive ihrer Bedeutung und potenziellen Anwendungsszenarien findet sich auf den Seiten von Restlet.

Caching und Security

Neben den bisher gezeigten Themen gibt es noch viele viele weitere Aspekte, die es beim Design eines „guten“ API zu beachten gilt. So hilft zum Beispiel ein ausgereiftes Caching-Konzept, ausgehende Client-Requests zu vermeiden oder alternativ durch vorgeschaltete Content Delivery Networks, Proxies oder andere Server aus deren Cache zu bedienen. Mittel zum Zweck ist auch hier wieder ein Bordmittel von HTTP. Während in HTTP 1.0 das Caching-Verhalten via Expires-Header nur relativ grob gesteuert werden kann, lässt in HTTP 1.1 der Cache-Control-Header deutlich mehr Optionen zu. Zusätzlich steht die Möglichkeit zur Verfügung via If-Modified-Since-Header (plus Last-Modified) oder If-None-Match-Header (plus ETag) ein Conditional GET an den Server abzusetzen. Stehen auf dem Server keine neueren Ressourcen zur Verfügung, so antwortet dieser mit dem Statuscode 304 (Not Modified) und einem leeren Body. Ansonsten wird die Anfrage wie ein normales GET behandelt.

Auch das Thema Security muss im Umfeld von REST neu betrachtet werden. Dies gilt insbesondere dann, wenn Systeme, die bis dato nur intern verwendet wurden, via API nach außen geöffnet werden. Da ein RESTful Service per Definition stateless sein sollte und somit serverseitig keine Session vorliegt, stellt sich die Herausforderung, wie die zur Authentifizierung und Autorisierung notwendigen Informationen innerhalb der Requests mitgegeben werden können, ohne dass der RESTful Service bei jeder Anfrage eine erneute Authentifizierungsanfrage an einen Authentication-Service absetzen muss. Eine mögliche und mittlerweile recht etablierte Lösung sieht vor, dass sich der Client einmalig an einem Authentication-Server authentifiziert und von dort ein zeitlich begrenztes signiertes Token bekommt (Abb. 1).

Abb. 1: Token-based Authentication

Abb. 1: Token-based Authentication

Dieses Token wird im Anschluss innerhalb des Authentication-Headers an den RESTful Service geschickt und kann dort auf seine Gültigkeit verifiziert werden. Damit das funktioniert, haben im Vorfeld der Authentication-Server und der RESTful Service einen Public Key ausgetauscht. Nutzt man als Token einen JSON Web Token (JWT), so können innerhalb des Tokens zusätzliche Daten als Key-Value-Paare in Form sogenannter Claims hinterlegt werden (Abb. 2). Dies ist insbesondere im Umfeld von REST-basierten Microservices von Interesse, da so allgemeine Daten, wie zum Beispiel die Rollen eines Nutzers, einfach und effizient durch die in einem Request involvierten Microservices gereicht werden können.

Abb. 2: JWT inklusive Claims

Abb. 2: JWT inklusive Claims

Academic vs. Pragmatic REST

Wie der bisherige Artikel zeigt, ist es gar nicht so schwer, ein gutes RESTful API zu designen. Wichtig ist, dass man nicht versucht, das Rad neu zu erfinden, sondern auf Standards und etablierte Patterns und Best Practices zurückgreift, um so die Erwartungshaltung des Anwenders zu erfüllen. Ebenfalls wichtig ist natürlich, dass einmal getroffene Designentscheidungen konsequent innerhalb des API zum Einsatz kommen. Wieso aber kommt es dann trotzdem immer wieder zu hitzigen Diskussionen über den korrekten Einsatz von REST?

Wahrscheinlich haben die wenigsten Entwickler von RESTful APIs jemals einen tieferen Blick in die Dissertation von Roy Thomas Fielding aus dem Jahr 2000 geworfen. Fielding beschreibt dort in Kapitel 5 einen „neuen“ Ansatz für netzwerkbasierte Softwarearchitekturen namens REST (Representational State Transfer). Das Interessante daran ist, dass wir in der Dissertation recht wenig über Endpoints, HTTP-Methoden oder gar JSON bzw. XML lesen. Es geht in der Arbeit vielmehr um einen Architekturansatz, der durch Begriffe wie Client-Server, Stateless, Caching, Uniform Interfaces, Layered System und Code on Demand geprägt ist. Das, was die meisten von uns unter REST verstehen, nämlich die Identifikation einer Ressource mittels eindeutigem URL sowie deren Manipulation durch eine Repräsentation der Ressource (JSON oder XML) im Zusammenspiel mit selbsterklärenden Messages (alias HTTP-Methoden) spielt in der Arbeit eher eine untergeordnete Rolle und wird dort lediglich als einer von mehreren Punkten mit „the four interface constraints“ bezeichnet. Bei „four“ wird jetzt der eine oder andere aufmerksame Leser innehalten und sich fragen: „Aber ich sehe hier doch bisher nur drei“:

  1. Identifikation via URL
  2. Manipulation via Repäsentation (z. B. JSON)
  3. Selbstbeschreibende Nachrichten (HTTP-Methoden)

Und genau hier liegt der Knackpunt. Als vierten Constraint führt Fielding „Hypermedia as the engine of application state“ (HATEOAS) auf. Die beiden folgenden Zitate von Fielding zeigen, welchen Stellenwert er persönlich den Begriffen Hypermedia und Hypertext im Kontext von REST beimisst:

  1. „If the engine of application state (and hence the API) is not driven by hypertext, then it cannot be RESTful and cannot be a REST API“.
  2. „A REST API should be entered with no prior knowledge beyond the initial URI … From that point on, all application state transitions must be driven by the client selection of server-provides choices …“

Fielding sagt also, dass ein RESTful API ohne vorheriger Kenntnisse jenseits des initialen URL genutzt werden können sollte. Der Aufruf dieses URLs liefert eine Liste von Links (als Teil der Payload oder alternativ als Linkheader) mit möglichen und sinnvollen Operationen.

HATEOAS

Zugegeben, das eben geschilderte Szenario klinkt für viele wahrscheinlich erst einmal ein wenig abstrakt. Dabei ist es genau das, was uns täglich im Internet begegnet, oder? REST ist also nichts anderes als die Abstraktion des uns bekannten Verhaltens des World Wide Web. Wir rufen einen URI auf und erhalten – neben der einen oder anderen Information – eine Liste von Links, die uns zeigen, welche Operationen genau in diesem Moment möglich bzw. erlaubt sind.

Kennengelernt haben wir dieses Vorgehen bereits bei der oben skizzierten Navigation via Pagination. Fragt man bei einem API einen Ausschnitt einer größeren Datenmenge an, so ermöglichen Linkreferenzen das Navigieren innerhalb der Datenmenge. Der Anwender des API kennt im Vorfeld nur die Semantik der Referenzen – prev entspricht zurück, next entspricht vorwärts – nicht aber die konkreten URLs. Es gibt mit dem RFC 5988 seit 2010 sogar einen Standard, der entsprechend Linknamen und ihre Semantik festlegt. Also bitte nicht das Rad neu erfinden, sondern wenn möglich und sinnvoll einen der dort aufgeführten Linknamen verwenden.

Bei der Navigation durch eine große Datenmenge ist es noch gut vorstellbar, dass das API generische Links für vor, zurück, erste oder letzte Seite liefern kann. Aber wie soll das bei den normalen Ressourcen eines RESTful API funktionieren? Nehmen wir uns noch einmal unser Beispiel zum Aufgeben, Ändern, Löschen und Abfragen von Bestellungen vor. Der initiale Aufruf des einzigen uns bekannten URI http://api.myshops.com/ würde uns einen HTTP Response mit dem Code 204 (No Content) liefern sowie eine Linkreferenz mit der Bezeichnung edit und dem URL http://api.myshops.com/orders. Aufgrund des oben genannten RFCs wüssten wir, dass wir mit diesem URL eine Bestellung aufgeben können (via HTTP POST).

Entscheidend für das Verständnis ist, dass wir natürlich schon vorher wussten, dass man über das API des Shops Bestellungen handhaben kann und auch das notwendige JSON-Format dafür kennen. Die konkret zu verwendenden URLs sind uns aber nicht bekannt und eigentlich auch egal. Geben wir nun mithilfe des zurückgelieferten URLs eine Bestellung auf, bekommen wir neben der Bestätigung (204 No Content oder 200 Ok) wieder Linkreferenzen zurück, die uns signalisieren, was man nun mit der eben aufgegebenen Bestellung anstellen kann.

Eine der Linkreferenzen ist typischerweise self mit dem Verweis auf die angelegte Ressource selbst. In unserem Fall wäre das zum Beispiel http://api.myshops.com/orders/123. Mithilfe dieses Links kann man nun jederzeit den Status der Bestellung abfragen. Eine weiterer Linkreferenz mit der Bezeichnung payment – ebenfalls Bestandteil des RFC 5988 – könnte uns signalisieren, wie wir die offene Bestellung bezahlen können: http://api.myshops.com/payments/123. Je nach gerade ausgeführter Aktion und den damit verbundenen serverseitigen Änderungen am Application State führen uns die Links also Schritt für Schritt durch die Anwendung bzw. in unserem Fall durch die möglichen Use Cases des Bestellprozesses. Für den Austausch der notwendigen Referenzinformationen hat sich in den letzten Jahren die Hypertext Application Language (HAL]) etabliert. Wem das Ganze nach wie vor ein wenig zu abstrakt ist, dem sei die HATEOAS-Demo von Heroku inklusive generischem HAL-Browser nahegelegt.

If it’s not REST …

Mal Hand auf Herz: Wer von uns geht in all seinen RESTful APIs so weit, wie eben in obigem Szenario beschrieben? Ich denke, die wenigsten. Aber wie ist nun das eigene API auf der nach oben offenen REST-Richterskala einzuordnen. Darf ich überhaupt noch das Wort REST im Zusammenhang mit meinem API in den Mund nehmen, ohne dabei einen Shitstorm aus der Ecke der REST-Puristen zu riskieren?

Als kleinen Lackmustest für die Einordnung des eigenen Webservice lässt sich sehr schön das Maturity Model von Leonard Richardson heranziehen. Richardson klassifiziert in seinem Modell Webservices je nach ihrem Support von URIs, HTTP und Hypermedia in drei unterschiedliche REST-Reifegrade (Abb. 3). Eigentlich sind es sogar vier, da es unterhalb der drei legitimen Level noch ein Level 0 gibt, das lediglich XML oder JSON via HTTP POST über die Leitung schickt und somit eher dem klassischem RPC-Modell entspricht. Dieses Level wird auch gerne mit RESTless bezeichnet, da es mit REST so gut wie keine Gemeinsamkeiten aufweist.

Abb. 3: Richardson Maturity Model

Abb. 3: Richardson Maturity Model

In Level 1 führt Richardson die von REST bekannten Ressourcen und somit unterschiedliche Endpoints pro Ressource ein. In einer Anwendung existieren bei diesem Modell in der Regel mehrere URIs. Allerdings wird weiterhin nur eine HTTP-Methode (meist POST) verwendet und auch sonst auf die erweiterten Möglichkeiten des HTTP-Protokolls, wie Header, Returncodes, Caching etc. verzichtet. Level 2 setzt genau da an, wo Level 1 aufhört. Den unterschiedlichen HTTP-Methoden werden Operationen auf den Ressourcen zugeordnet (Abfragen der Ressource via GET, Anlegen via POST oder PUT, Ändern via PUT, partielles Ändern via PATCH und Löschen via DELETE). Und auch die HTTP-Statuscodes werden sinnvoll verwendet, um so zu signalisieren, was auf dem Server mit den Ressourcen passiert ist – oder eben nicht. Erst in Level 3 – alias „the glory of REST“ – führt Richardson Hypermedia und somit ein sich selbsterklärendes System ein.

Fazit

O.k., einigen wir uns also darauf, dass – nach der reinen Lehre – wahrscheinlich die wenigsten von uns bisher den heiligen REST-Olymp erklommen haben. Aber ist das wirklich so schlimm? Wichtig ist doch, dass am Ende ein sprechendes API entsteht, das für den Anwender, also den Entwickler, verständlich ist und eine entsprechende Stabilität mit sich bringt. Vinay Sahni, Gründer von Enchant, schrieb dazu in seinem Blog: „An API is a developer‘s UI – just like any UI, it‘s important to ensure the user‘s experience is thought out carefully!“ Treffender kann man es wohl kaum formulieren.

Aber darf ich mein API nun überhaupt RESTful nennen, wenn ich nach Richardson lediglich ein Level unterhalb von 3 erreicht habe? Ich persönlich bin da eher pragmatisch als religiös veranlagt. Wenn mein API die Bedingungen von Level 2 erfüllt, dann würde ich es durchaus als RESTful bezeichnen, bei Level 1 dagegen eher nicht.

Was aber, wenn ich auf einen bekennenden Level-3-Vertreter treffe, der keine Wahrheit neben der seinen akzeptiert? Oder anders formuliert: Sollte ich in einer solchen Situation auf Teufel komm raus darauf bestehen, ein RESTful API entworfen zu haben? Vielleicht fehlt uns einfach nur ein Terminus für die Formel RESTful minus HATEOAS? Wie wäre es mit RESTalike?

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

1 Kommentar auf "RESTful-API-Design: Eine Einführung"

avatar
400
  Subscribe  
Benachrichtige mich zu:
MrGreen
Gast

Wo ist denn nun der Unterschied bei „Listing 4: Abfragen mit Filtern“ zu sehen?

X
- Gib Deinen Standort ein -
- or -