Dlaczego 99% firm, które tworzą API RESTowe kłamie?

Bartek Andrzejczak / @baandrzejczak

Może powtórzymy angielski?

Google



Ambrose Little

Czym właściwie jest REST?

This chapter introduces and elaborates the Representational State Transfer (REST) architectural style for distributed hypermedia systems (...)
(...) a style is a named set of constraints on architectural elements that induces the set of properties desired of the architecture.
"Architectural Styles and the Design of Network-based Software Architectures"
Roy T. Fielding

Ograniczenia narzucane przez REST

  • Podział klient-serwer
  • Bezstanowość serwera
  • Cache
  • Jednolity interfejs
    • Identyfikacja zasobów
    • Manipulacja zasobami przez reprezentacje
    • Samoopisujące się wiadomości
    • HATEOAS
  • Podział na warstwy
  • Code-on-demand (Opcjonalne)

Hypermedia As The Engine Of Application State

Role, artefakty, zdarzenia oraz reguły Scruma są niezmienne i choć możliwe jest wykorzystanie tylko wybranych jego elementów, wynikiem takiego postępowania nie będzie Scrum. Scrum istnieje tylko w swojej pełnej postaci i sprawdza się doskonale w roli ramy dla innych technik, metodyk czy praktyk.
Scrum Guide

Jak ważny jest HATEOAS?

In order to obtain a uniform interface, multiple architectural constraints are needed to guide the behavior of components. REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state. These constraints will be discussed in Section 5.2.
"Architectural Styles and the Design of Network-based Software Architectures"
Roy T. Fielding
The name “Representational State Transfer” is intended to evoke an image of how a well-designed Web application behaves:
  • a network of web pages (a virtual state-machine)
  • where the user progresses through the application by selecting links (state transitions)
  • resulting in the next page (representing the next state of the application) being transferred to the user and rendered for their use

Co daje nam HATEOAS?

Kiedy HATEOAS jest stratą czasu?

  • Brak wyraźnego flow aplikacji
  • CRUD
  • Małe API

Zalety

  • Sterowanie przepływem danych w aplikacji
  • Sterowanie dostępnymi podzasobami
  • Luźniejsze związanie serwera i klienta
  • Dodatkowa dokumentacja API (jednak niewystarczająca)

Wady

  • Więcej pracy
  • Więcej transferu
  • Brak ekspertów w temacie
  • Tworzenie API dla nieistniejącego klienta

Dla jakiego klienta nadaje się HATEOAS?

Format linków

HATEOAS na serwerze

Jersey Declarative Linking

Najprostszy przykład


@Path("/people")
public class PeopleResource {
    @GET
    public List<Person> list() { ... }
    @GET
    @Path("/{id}")
    public Person get(@PathParam("id") String personId) {
        return new PeopleRepository().find(personId);
    }
}

public class Person {
    public String name;
    @InjectLink(resource=PeopleResource.class)
    public URI self;
}
                        

{
    "name": "Luke Skywalker",
    "self": "http://swapi.co/api/people"
}
                        

Parametry ścieżek


@Path("/people/{id}")
public class PersonResource {
    ...
}
public class Person {
    public String name;
    @InjectLink(
        resource=PlanetResource.class,
        bindings={
            @Binding("${resource.homeworldId}")
        }
    )
    public URI homeworld;
    @JsonIgnore
    public String homeworldId;
}
                        

{
    "name": "Luke Skywalker",
    "homeworld": "http://swapi.co/api/planets/1"
}
                        

Stałe ścieżki


public class Root {
    @InjectLink("/films", rel="films")
    public URI films;
}
                        

{
    "films": "http://swapi.co/api/films"
}
                        

Linki w nagłówkach


@InjectLinks(
    value=@InjectLink("planets/${resource.homeworldId}"),
    rel="homeworld"
)
public class Person {
    public String name;
    @JsonIgnore
    public String homeworldId;
}
                        

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
Link: <http://swapi.co/api/planets/1>; rel="homeworld"

{
    "name": "Luke Skywalker"
}
                        

Plusy

  • Wybór pomiędzy linkami w nagłówkach i w reprezentacjach

Minusy

  • Brak możliwości przesyłania tylko niektórych linków
  • Silne połączenie reprezentacji z zasobami
  • Annotation hell

JAX-RS 2.0

Tylko imperatywna konstrukcja linków.

@Path("/people/{id}")
public class PersonResource {
    @GET
    public Response get(@PathParam("id") String personId) {
        return Response
                .ok(new PeopleRepository().find(personId))
                .links(Link.fromResource(PersonResource.class)
                            .rel("self").build(personId))
                .build();
    }
}
                        

HTTP 200 OK
Content-Type: application/json
Link: <http://swapi.co/api/people/1>; rel="self"
                        
Ścieżki można pobierać z metod...

@Path("/people/{id}")
public class PersonResource {
    @GET
    public Response get(@PathParam("id") String personId) {
        return Response
                .ok(new PeopleRepository().find(personId))
                .links(Link
                        .fromMethod(PersonResource.class, "films")
                        .rel("films").build(personId))
                .build();
    }

    @GET
    @Path("/films")
    public Response films(){ ... }
}
                        

HTTP 200 OK
Content-Type: application/json
Link: <http://swapi.co/api/people/1/films>; rel="films"
                        
...lub hardcodować

@GET
public Response get(@PathParam("id") String personId) {
    Person person = new PeopleRepository().find(personId);
    String fatherId = person.findFather();
    return Response
            .ok()
            .links(Link
                    .fromPath("/people/{id}")
                    .rel("father").build(fatherId))
            .build();
}
                        

HTTP 200 OK
Content-Type: application/json
Link: <http://swapi.co/api/people/darthVader>; rel="father"

{
    "name": "Luke Skywalker"
}
                        

Plusy

  • Brak ingerencji w klasy reprezentacji
  • Implementacja przez wiele różnych frameworków

Minusy

  • Płaska struktura linków
  • Nagłówek trudniej jest analizować niż reprezentację

spring-hateoas

Aby dodawać linki do klasy, musi ona rozszerzać klasę ResourceSupport - linki staną się częścią reprezentacji.

public class Person extends ResourceSupport {
    public String name;
    public String height;
    public String mass;
    public String hair_color;
}
                        
Linki dodajemy bezpośrednio do reprezentacji

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

@Controller
public class PersonController{
    @RequestMapping("/people/{id}")
    @ResponseBody
    public HttpEntity<Person> get(@PathVariable("id") String personId) {
        Person person = new PeopleRepository().find(personId);
        person.add(
            linkTo(methodOn(PersonController.class).get(personId))
                .withSelfRel()
        );
        return new ResponseEntity<Person>(person, HttpStatus.OK);
    }
}
                        
Wynik:

{
    "name": "Luke Skywalker",
    "height": "1.72 m",
    "mass": "77 Kg",
    "hair_color": "Blond",
    "_links":{
        "self":{
            "href": "http://swapi.co/api/people/1"
        }
    }
}
                        

Plusy

  • Minimalna ingerencja w klasy reprezentacji
  • Możliwość zagnieżdżania linków
  • Obsługa różnych formatów linków
  • Wsparcie JAX-RS

Minusy

  • Narastająca logika w kontrolerach, w przypadku linkowania bardziej skomplikowanych struktur (np. kolekcji)

HATEOAS w kliencie

Java (JAX-RS)

Linki w reprezentacjach

{
    "name": "Luke Skywalker",
    "links": {
        "homeworld": {
            "uri": "http://swapi.co/api/planets/1"
        }
    }
}
                        

final Person table = target.request().get(Person.class);

URI homeworldURI = table.getLinks().getHomeworld().getUri();
WebTarget homeworldTarget = client.target(homeworldURI);
                        
Linki w nagłówkach

HTTP 200 OK
Content-Type: application/json
Link: <http://swapi.co/api/planets/1>; rel="homeworld"
Link: <http://swapi.co/api/species/1>; rel="species"

{
    "name": "Luke Skywalker"
}
                        

final Response response = target.request().get();

URI homeworldURI = response.getLink("homeworld").getUri();
URI speciesURI = response.getLink("species").getUri();
                        

JavaScript (AngularJS)

Linki w reprezentacjach

{
    "name": "Luke Skywalker",
    "links": {
        "father": {
            "uri": "http://swapi.co/api/people/2"
        }
    }
}
                        

var personResource = $resource("/api/people/:personId");

var luke = personResource.query({personId: 1}, function () {
    var lukesFather = $resource(luke.links.father)
                                .query(null, function () {
        console.log(lukesFather);
    });
});
                        
Linki w nagłówkach

HTTP 200 OK
Content-Type: application/json
Link: <http://swapi.co/api/people/2>; rel="father"

{
    "name": "Luke Skywalker"
}
                        

var personResource = $resource("/api/people/:personId");

var luke = personResource.query({personId: 1}, function () {
    var lukesFather = firstPerson.resource("father")
                                .query(null, function () {
        console.log(lukesFather);
    });
});
                        

Link: </people>;
    rel="people";
    actions="[
        {'name':'add','method':'POST'},
        {'name':'list','method':'GET'}
    ]"

                        

$http.get("/").success(
    function (root) {
        var peopleList = root.links.people.list();
        root.links.people.add(
            {
                "name": "Darth Vader",
                "height": "1.8"
            }
        );
    }
);
                        
https://github.com/bandrzejczak/bpm-console-gui
https://github.com/bandrzejczak/bpm-console-rest

Podsumowanie

  • HATEOAS
  • Maszyna stanów
  • Rosnące wsparcie bibliotek

swapi.co

Pytania?

Dzięki!