Der optimale Aufbau eines RESTful Webservices

Da ich in letzter Zeit häufiger mit APIs, insbesondere mit REST APIs, zu tun hatte, wollte ich euch einfach mal mitteilen, wie ich mir eine gut strukturierte REST API vorstelle. Die meisten RESTful Webservices sind leider nicht optimal strukturiert. Selbst die ganz großen APIs wie die von Twitter haben meiner Meinung nach noch viel Verbesserungsbedarf.

Bevor ich jetzt jedoch anfange auf die Einzelheiten einzugehen, gibt es hier noch mal eine kleine Einführung in die Welt der RESTful Webservices.
REST bedeutet Representational State Transfer und damit ist eigentlich Adressaufbau für einen bestimmten Webservice gemeint. Klingt etwas kompliziert, deshalb erkläre ich es noch mal etwas anderes: Wenn sich zwei verschiedene Services im Internet austauschen wollen, dann brauchen Sie für eine bestimmte Ressource immer eine eindeutige Adresse die sie aufrufen können. Wenn ein WordPress-Blog zum Beispiel die Tweets eines bestimmten Benutzers laden will, dann sollte Twitter im Idealfall eine Adresse wie /users/123/tweets zur Verfügung stellen.

Kling alles super, problematisch ist nur, dass es für REST keinen Standard gibt, sondern eigentlich nur als Idee oder Prinzip existiert. Somit kann jeder seine API RESTful nennen, obwohl diese gar nicht dem Grundsatz entspricht. Ich habe daher einige Regeln aufgestellt, die ich für eine gute REST API als sinnvoll erachte.

Hinweis: Die folgenden Beispiele beziehen sich zwar alle auf Twitter, existieren in dieser Form allerdings nicht.

Authentifizierung

In der Regel will man seinen Webservice ja nur bestimmten Services zur Verfügung stellen und daher sollte man sie irgendwie mittels einer Authentifizierung schützen. Am einfachsten ist eine HTTP Basic- oder Digest-Authentifizierung, weil es zum Browserstandard gehört und so gut wie jede HTTP-Lib damit umgehen kann.

GET /users/123/statuses HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=

HTTP-Methoden

Einer der größten Mankos bei den meisten REST APIs ist das fehlen der HTTP-Methoden wie GET, POST, PUT oder DELETE. Dabei lässt sich dem Webservices damit wunderbar sagen was er mit einer bestimmten Ressource tun soll. Bei GET werden Daten abgefragt, bei POST werden neue Daten angelegt, bei PUT werden Daten geändert und bei DELETE – ihr könnt es euch sicher schon denken – werden Daten gelöscht.

Alle Tweets vom Benutzer 123 holen:

GET /users/123/statuses HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=

Tweet 456 von Benutzer 123 holen:

GET /users/123/statuses/456 HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=

Benutzer 123 bearbeiten:

PUT /users/123 HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=
Content-Typ: application/json

{"data":"Beispieldaten im JSON-Format"}

Tweet erstellen:

POST /users/123/statuses HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=
Content-Typ: application/json

{"data":"Beispieldaten im JSON-Format"}

Tweet löschen:

DELETE /users/123/statuses/456 HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=

Der Vorteil bei diesem Aufbau ist, dass man nicht noch unnötige Verben in der URL einsetzten muss. Twitter benutzt in seiner aktuellen API beispielsweise GET statuses/show/:id um einen bestimmten Tweet zu laden. Das /show könnte man auch einfach weglassen, da man mit GET bereits festlegt, dass man Daten „angezeigt“ bekommen möchte.

Datenformate

Ich möchte an dieser Stelle gar nicht so genau darauf eingehen welches Format man für den Datenaustausch verwenden soll, sondern wie man das Format flexibel ändern kann. Und genau für diesen Fall gibt es bereits einen weit verbreiteten HTTP-Header Parameter:

GET /users/123/statuses/456 HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=
Accept: application/json

Mit dem Accept-Parameter lässt sich dem Webservice sagen in welchem Format ich die Ressource geliefert bekommen möchte. Der große Vorteil dabei ist, dass man so auch den normalen Websitebenutzer mit der gleichen URL beliefern kann, weil der Browser den Accept-Parameter automatisch auf text/html setzt.

API Version

Wenn APIs größere Änderungen erfahren, weil sich zum Beispiel die Datenstruktur ändert, dann wird oft einfach eine neue URL für die neue Version gebaut:
http://apiv2.webservice.com/users/123 oder noch schlimmer http://api.webservice.com/v2/users/123
Auch dieses Problem lässt sich relativ einfach mit dem Accept-Parameter aus dem HTTP-Header lösen.

GET /users/123/statuses/456 HTTP/1.1
Host: twitter.com
Authorization: Basic dGVzdDoxMjM=
Accept: application/my.restful.webservice-v2+json

Anhand des akzeptierten Mediatyps, kann die API ganz einfach die Versionsnummer auslesen und eine entsprechend andere Datenstruktur ausliefern.

Fehlercodes

Wenn während der Verarbeitung einer API-Abfrage ein Fehler auftritt, liefert die Schnittstelle meist nur einen Error 500 und eine sprechende Fehlermeldung. Für den Entwickler ist das meistens auch ausreichend, weil er nur die Fehlermeldung lesen muss, aber eine Maschine kann damit nicht viel anfangen. Sie würde auf jede Fehlermeldung gleich reagieren. Deshalb ist es sinnvoll bei Fehlern auch immer einen entsprechenden HTTP-Statuscode mitzuliefern. Zum Beispiel 403 Forbidden, wenn der Client keine Berechtigung hat die angefragte Ressource abzurufen, oder 501 Not Implemented, wenn eine Ressource zwar schon vorhanden ist, aber noch keine Funktionalität dafür hinterlegt wurde.

Natürlich kann der Error-Code auch innerhalb des Bodys eingesetzt werden, aber je mehr Standards vom HTTP-Protokoll verwendet werden, desto einfacher kann die API später von anderen verarbeitet werden. Man muss keine 10-Seitige Dokumentation über eigene Fehlercodes schreiben, sondern verweist einfach auf die Statuscodes von HTTP und so können auch Maschinen einfacher auf unterschiedliche Fehlerfälle reagieren.

Zusammenfassung / Fazit

Wenn ihr mal mit SOAP gearbeitet habt und dann mit einer API arbeiten müsst, die sich an all diese Regeln hält, werdet ihr sehr schnell merken wie leicht dadurch das Abfragen der Ressourcen wird. Es wirkt intuitiver und weniger komplex als SOAP oder eine andere nicht-RESTful API.

Zum Abschluss nochmal ein kurze, stichwortartige Zusammenfassung:

  1. Jede Ressource bekommt eine URL
  2. Mittels HTTP-Methoden wird die Operation für die Ressource ausgewählt
  3. Gewünschte Datenformate und Version werden über den Mediatyp (Accept-Parameter) im HTTP-Header angegeben
  4. Fehler werden weitesgehend über die HTTP-Statuscodes ausgeliefert

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.