On Github webplatformz / rest_in_peace
"Representational state transfer (REST) is an architectural style consisting of a coordinated set of architectural constraints applied to components, connectors, and data elements, within a distributed hypermedia system."
A concrete implementation of a REST Web service follows four basic design principles:
REST ist eigentlich ein Paradigma mit "Quasi Standard".
So ist ein Web-Dienst, der lediglich unveränderte Seiteninhalte nach dem Internetstandard HTTP anbietet, bereits REST-konform.
GET http://transport.opendata.ch/v1/locations ==> { "stations": [] }
Antwort
GET /connections?from=oerlikon&to=wallisellen&limit=2 DELETE /connections?from=oerlikon&to=wallisellen&limit=2
GET https://api.github.com/repos/bmillo/recycle_calendar GET http://isvat.appspot.com/DE/197493877/ GET api.openweathermap.org/data/2.5/weather?q=Zurich,ch
POST GET PUT DELETE
!=
CRUD
Entspricht dem READ und DELETE von CRUD.
GET http://transport.opendata.ch/v1/stations DELETE http://transport.opendata.ch/v1/stations/1234
Beide Methoden können für CREATE und UPDATE verwendet werden.
POST http://transport.opendata.ch/v1/stations { name : "Zürich Hauptbahnhof" } PUT http://transport.opendata.ch/v1/stations/8765 { name : "Zürich Hauptbahnhof" }
WTF?
Der Schlüssel liegt in der Idempotenz
"Send and send and send my friend, it makes no difference in the end."
- James Beninger
POST ist nicht idempotent:
POST http://transport.opendata.ch/v1/stations { name : "Zürich Hauptbahnhof" } ==> 201 Created Location: /stations/4444
Was passiert, wenn ich diesen Request mehrmals ausführe?
POST http://transport.opendata.ch/v1/stations { name : "Zürich Hauptbahnhof" } ==> 201 Created Location: /stations/4444
and again!
POST http://transport.opendata.ch/v1/stations { name : "Zürich Hauptbahnhof" } ==> 201 Created Location: /stations/4445
Wir haben soeben dieselbe Location zwei mal erstellt.
PUT ist idempotent:
PUT http://transport.opendata.ch/v1/stations/4444 { name : "Zürich Hauptbahnhof" } ==> 201 Created Location: /stations/4444
Was passiert, wenn ich diesen Request mehrmals ausführe?
PUT http://transport.opendata.ch/v1/stations/4444 { name : "Zürich Hauptbahnhof" } ==> 201 Created Location: /stations/4444
and again!
PUT http://transport.opendata.ch/v1/stations/4444 { name : "Zürich Hauptbahnhof" } ==> 201 Created Location: /stations/4444
Wir haben die erste Location einfach überschrieben.
Bei POST lassen wir den Server entscheiden, unter welcher Location die Resource erstellt wird.
Bei PUT sagen wir dem Server, unter welcher Location die Resource erstellt/überschrieben werden soll.
PUT http://transport.opendata.ch/v1/locations/4444 POST http://transport.opendata.ch/v1/locations
Achtung: Natürlich liegt es an der Implementation des Servers, ob PUT & POST auch wirklich idempotent ist oder nicht!
POST darf verwendet werden, wenn die Query Parameter eine gefährlich grosse Länge annehmen könnten. Zum Beispiel, wenn ich n Rufnummern einem Standort zuweisen möchte:
PUT /numbers/1,2,3,4,5,6,7,...,756879895 { "locationId" : 1337 }
POST /numbers/locationAssignment { "numberIds" : [ 1,2,3,... ], "locationId" : 1337 }
Verwende immer Nomen, um die Ressourcen zu benennen.
Verwende immer Plural, ausser es handelt sich wirklich um eine einzelne Ressource und nicht eine Collection.
Wir exposen mit einer REST API die Ressource =Nomen
und nicht die Operationen darauf =Verben
Die HTTP Methode sagt uns, was wir auf dieser Ressource tun.
POST /locations // DO POST /locations/createLocation // DON'T DELETE /locations/1234 // DO DELETE /deleteLocation/1234 // DON'T
// nope { employee : { employeeId : 111, employeeName : "John" } }
skip the prefix!
{ employee : { id : 111, name : "John" } }
GET und DELETE haben keinen Payload.
Einschränkungen für die Antwort (z.B. search-parameters) gehören in die URL-Parameter.
GET /locations?canton=Aargau&county=Baden
Design your API
JAX-RS with Jersey
Automatisches Serialisieren und Deserialisieren
Der Accept-Header gibt an, wie die Antwort aussehen soll.
Accept: application/json Accept: application/xml
Beispiel
GET /employees Accept: application/json
@GET public Employee[] getEmployees() { ... } ==> [{ name : "John Wayne" }, ..., ]
@Path("/regservice/employees") public class EmployeeRestService { ... }
Client:
GET /employees/1234
Server:
@GET @Path("/{employeeId}") public Employee getEmployee(@PathParam("employeeId") String employeeId) ==> { id : "1234", name : "John Wayne" }
Client:
GET /employees?name=John
Server:
@GET public Employee getEmployees(@QueryParam("name") String name) ==> { id : "1234", name : "John Wayne" }
Client:
POST /employees { name : "Chuck Norris" }
Server:
@POST public void createEmployee(Employee employee) ==> 204 No Content
GET /employees/123 ==> { id: 123, name: "John", surname : "Doe", departmentId : 1234 }
GET /employees/123?expand=department ==> { id: 123, name: "John", surname : "Doe", department : { id: 1234, name: "MYA", street: "Hardturmstrasse 4", zipCode: 8005, city: "Zürich" } }
PATCH wird zurzeit noch nicht flächendeckend unterstützt
Initialer Request
GET /employees/123 ==> { id: 123, name: "John", prename : "Doe", departmentId : 1234, salary : 5000 }
Nach RFC 6902
PATCH /employees/123 Content-Type: application/json-patch+json [ { op: "replace", path: "salary", value: 6000 }, { op: "replace", path: "name", value: "Joe" } ] ==> 200 OK
GET /employees/123 ==> { id: 123, name: "Joe", prename : "Doe", departmentId : 1234, salary : 6000 }
Die HEAD Methode entpspricht einem GET-Aufruf, jedoch wird dabei der Body der Response weggelassen.
Die Header der HEAD Response müssen identisch zu dem Header der GET Response sein.
HEAD /employees/123 ==> 200 OK Content-Length: 2885
Die OPTIONS Methode erlaubt dem Client die Optionen und Anforderungen des Servers bzw. einer Ressource zu erhalten.
OPTIONS /employees ==> 200 OK Allow: HEAD,GET,PUT,DELETE,OPTIONS
ABER: OPTIONS ist nur bei wenigen Rest-API's implementiert.
Als Teil vor der Ressource
GET http://transport.opendata.ch/v1/locations
Als URL-Parameter
GET http://transport.opendata.ch/locations?v=1
Versionierung durch Accept Header
GET http://transport.opendata.ch/locations Accept: application/vnd.opendata.v1+json
Versionierung durch Custom Header
GET http://transport.opendata.ch/locations api-version: 1
Für die Fehlerkommunikation zwischen Server und Client sind die HTTP Statuscodes vorgesehen.
Die Anfrage ist fehlerhaft aufgebaut
POST /employees { name : "John Doe"; salary : 5000 } ==> 400 Bad Request
Der Client muss sich authentisieren
In der Response schickt der Server ein WWW-Authenticate Header, um die Authentisierung zu veranlassen.
GET /secrets ==> 401 Unauthorized WWW-Authenticate: Basic realm="My Server"
Die Angefragte Ressource kann nicht gefunden werden
GET /employees/999999999 ==> 404 Not Found
Das Bearbeiten der Anfrage wird vom Server für diese Ressource nicht bereitgestellt. z.B. PUT auf einer Readonly Ressource
PUT /readOnlyResource/1 { title : "RESTful Web Services Cookbook" pages : 314 } ==> 405 Method Not Allowed
Die geschickten Daten sind syntaktisch korrekt, werden jedoch wegen semantischer Fehler abgelehnt
POST /employees { employeesname : "John Doe", employeessalary : 5000 } ==> 422 Unprocessable Entity
Ein Teekrug erhält eine Anweisung zum Kaffee brühen.
Aprilscherz der IETF aus dem Jahre 1998
Sammel Statuscode welcher bei unerwarteten Fehlern geschmissen wird
POST /employees { name : "John Doe" salary : 5000 } ==> 500 Internal Server Error
GET /employees/123 ==> { id: 123, name: "John", departmentId : 1234, salary : 6000 links : [{ rel: "self", href: "/employees/123" }, { rel: "boss", href: "/employees/632" }] }