How to get the BEST
from your REST     

Asmir Mustafic

(API)

WHO AM I?

Asmir Mustafic

Work

Software architect and consultant
(mainly PHP)

Helping companies deal with their tech stack

Open source

  • Doctrine (occasional contributor)
  • JMS Serializer  (maintainer)
  • HTML5-PHP (core dev)
  • XSD2PHP (author)
  • Twital (author)
  • Many SOAP-related packages...

contributed/contributing to:

What to expect?

  1. Theory about REST API

    • what, how and why

  2. Some PHP implementations

    • ​client & server
    • hints and tricks

    • no browsers or mobile 

What are WEB APIs?

Web APIs are the defined interfaces through which interactions happen between an enterprise and applications that use its assets. (Wikipedia)

A way to communicate between objects, processes, servers, server-client ...

Where do we use WEB-APIs?

How do we want WEB-APIs

  • Rich
  • Fast
  • Up
  • Stable
  • Easy to change
  • Fast to develop
  • (and many more)

not easy to achieve all of them!

Many already
 tried to do it

REST, XML, SOAP, RPC, JSON-RPC, XML-RPC, WSDL, JAX, COBRA, CUSTOM, CSV-FTP, JRMI, COM, DCOM, Thrift, IDL, gRPC...

REST(FUL) API

Roy Fielding (2000)

Fundamentals

Client - Server

Separation of responsibilities

Stateless

The server stores no information about the client,
so the architecture scales well

Cacheable

The client has access to resources between it and the server,
so che communication is faster

Layered

Allows intermediary nodes transparently,
for scalability, cacheability, encapsulation, extension...

Uniform Interface

Well know verbs and provide all the information needed to consume the service,
so it is easier to implement it, to scale it, to improve it...

REST Fundamentals

  1. Client - Server
  2. Stateless
  3. Cacheable
  4. Layered
  5. Uniform interface

HTTP
already does it!

(almost 20 years ago)
(and no body knew it)

Client - Server

HTTP is definitely client-server

Stateless

sessions and cookies
are implemented at application level

HTTP is just a transport protocol

Cacheable

Cacheable!

almost everybody forget it!

...or invents a way to make it not cacheable

Rich set of Cache headers & policies

Layered

ME > application > application-wrapper > webserver > load-balancer > reverse-proxy > firewall > cdn > proxy > local-cache > browser >  YOU

Allows encapsulation in other protocols (see HTTP2)

Uniform Interface

resources, resource identification (URIs), resource types (MIME), operations (verbs)

REST (and  HTTP) Fundamentals

  1. Client - Server
  2. Stateless
  3. Cacheable
  4. Layered
  5. Uniform interface

Notes

Personal experience,
often small mistakes are the source of big problems

Stateless

  • Horizontal scalability amost for free
  • Simpler logic
  • Easier to reproduce bugs
  • Less context overload
  • Easier to document
  • Easier to develop
  • (no end to advantages IMO...)

Stateless

  • No cookies
  • No sessions

 

Do not store any kind of state information

Cacheable

  • Unique URL per resource
    • use redirect for convenience URLs 
  • Use cache headers
  • Design for cache invalidation
GET /foo
Host: foo.com
HTTP/1.1 200 OK
Host: foo.com
Expires: Mon, 10 Dec 2020 14:10:20 GMT
HTTP/1.1 200 OK
Host: foo.com
Last-Modified: Mon, 10 Dec 2010 14:10:20 GMT
HTTP/1.1 200 OK
Host: foo.com
ETag: XXXYYYZZZ
HTTP/1.1 200 OK
Host: foo.com
Cache-Control: max-age=60

Cache Headers

GET /foo
Host: foo.com
HTTP/1.1 200 OK
Host: foo.com
Cache-Control: max-age=60, public
HTTP/1.1 200 OK
Host: foo.com
Cache-Control: max-age=60, private

Browser + Proxy (squid/varnish)

Browser only

Caching (types)

GET /foo
Host: foo.com
HTTP/1.1 200 OK
Host: foo.com
Cache-Control: max-age=60, stale-if-error=3600

Display the cached version in case of errors when fetching a fresh version

Caching (Fault Tolerance)

GET /foo
Host: foo.com
HTTP/1.1 200 OK
Host: foo.com
Cache-Control: max-age=60, stale-while-revalidate=3600

Display the cached version while fetching in background the fresh version

Caching (User Experience)

Multiple URIs 

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
{
    "plate":"B-XLA870",
    "type": "car",
    "status": "new",
    ...
}
GET /car/73
Host: foo.com
Accept: application/json

How to get the same car "by plate code", but keeping the unique URI and having it cacheable?

Multiple URIs

HTTP/1.1 301 Moved Permanently
Cache-Control: max-age=60
Location: http://foo.com/car/73
GET /car/by-plate/B-XLA870
Host: foo.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60

{
    "plate":"B-XLA870",
    "type": "car",
    "status": "new",
    ...
}

Cache invalidation

There are two hard things in computer science: cache invalidation and naming things

(who?)

It is hard, but we have to do it!

If we have strictly implemented the hints in "Uniform Interface" and "Stateless" then
it is easier

Invalidate (1)

GET /foo
Host: foo.com
Accept-Language: en
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Language: en
Content-Type: text/html
Vary: Content-Language,
      Content-Type
GET /foo
Host: foo.com
Accept-Language: de
GET /foo
Host: foo.com
Accept: text/xml
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Language: de
Content-Type: text/html
Vary: Content-Language,
      Content-Type
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Language: de
Content-Type: text/xml
Vary: Content-Language,
      Content-Type
PURGE /foo
Host: foo.com

Invalidate (2)

GET /user/friends
Host: foo.com
Accept: application/json
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Type: application/json
Vary: Content-Type

{
    "friendsCount": 2
}
GET /user
Host: foo.com
Accept: application/json
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Type: application/json
Vary: Content-Type

[
    {friend1},{friend2}
]

We have to invalidate multiple resources

PURGE /user
Host: foo.com
PURGE /user/friends
Host: foo.com

Adding a new friend?

Invalidate (2)

GET /user/friends
Host: foo.com
Accept: application/json
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Type: application/json
Vary: Content-Type
Cache-Tags: user-343

{
    "friendsCount": 2
}
GET /user
Host: foo.com
Accept: application/json
HTTP/1.1 200 OK
Cache-Control: max-age=360
Content-Type: application/json
Vary: Content-Type
Cache-Tags: user-343, other

[
    {friend1},{friend2}
]

Invalidate (2)

When the user gets a new friend...


$cache->invalidateByTag('user-343');
BAN /
Cache-Tags: user-343

Not yet standardized... but works!

Uniform Interface

  1. Resource = URI
    • Different representation have the same URI
  2. Resources are altered using resource representations
    • use the right "verb" to alter resources
    • different representation for different proposes
  3. Self descriptive representations
    • ​​how to process the representation
  4. Transitions  are inferred from the representation
    • ​HATEOAS (will talk about in a minute)

Is not about URIs

http://www.example.com/user/1/detail
http://www.example.com/locate.py?user=1&serverMode=5&uuid=amihs

But having nice URI helps to focus

Both examples are good URIs

GET /me
Host: foo.com
Accept-Language: de;q=0.8,en;q=0.6
Accept: application/json
HTTP/1.1 200 OK
Host: foo.com
Content-Language: en
Content-Type: application/json

....
GET /me
Host: foo.com
Accept-Language: de;q=0.8,en;q=0.6
Accept: text/html,application/json
HTTP/1.1 200 OK
Host: foo.com
Content-Language: de
Content-Type: text/html

....

JSON

HTML

Different representations

GET /me
Host: foo.com
Accept: application/json
PATCH /me
Host: foo.com
Content-Type: application/json

{
    "newName": "alter ego"
}
HTTP/1.1 200 OK
Host: foo.com
Content-Type: application/json

{
    "name": "Asmir",
    "status": "some"
}
POST /me
Host: foo.com
Content-Type: application/json

{
    "name": "alter ego",
    "newState": "something different"
}

Altering resources using different representations

GET /users
HTTP/1.1 200 OK

[user1, user2, ..., userN]

What if you have 100M users?

Collections

GET /users
HTTP/1.1 200 OK
Accept-Ranges: items
Content-Range: items 0-99/10000000

[user1, user2, ..., user100]

Pagination

Return the default pagination range
(100 users per page)

GET /users
Range: items=30-60
HTTP/1.1 208 Partial Content
Accept-Ranges: items
Content-Range: items 30-60/10000000

[user30, user31, ..., user60]

Pagination (2)

Do not want to count all the users?

GET /users
Range: items=30-60
HTTP/1.1 208 Partial Content
Accept-Ranges: items
Content-Range: items 30-60/*

[user30, user31, ..., user60]

Pagination (infinite)

GET /users
Range: items=0-9
HTTP/1.1 208 Partial Content
Accept-Ranges: items
Content-Range: items 0-9/*
Cache-Control: max-age=3600
Vary: Content-Range

[user0, ..., user9]

Pagination (caching)

Do not forget the "Vary" header!

HATEOAS

The principle is that a client interacts with a network application entirely through hypermedia provided dynamically by application servers.

A REST client needs no prior knowledge about how to interact with any particular application or server beyond a generic understanding of hypermedia
(Wikipedia)

HTTP/1.1 200 OK
Content-Type: application/json

{
    "users":{
        "verb": "GET",
        "uri": "http://foo.com/user"
    },
    "user-add":{
        "verb": "PUT",
        "uri": "http://foo.com/user/{id}"
    },
    "messages":"http://foo.com/user/{id}/messages",
    "new-messages":"http://foo.com/user/{id}",
    .....
}
GET /
Host: foo.com
Accept: application/json

HATEOAS

(entrypoint)

https://tools.ietf.org/html/rfc6570

HATEOAS

(actions)

HTTP/1.1 200 OK
Content-Type: application/json

{
    "name": "mike",
    "age": 5,
    "_links": {
        "delte": {
            "uri": "http://foo.com/delete?id=999999822",
            "verb": "DELETE"
        },
        "self":{
            "uri": "http://foo.com/user/5",
            "verb": "DELETE"
        }
    }
}
GET /user/5
Host: foo.com
Accept: application/json

HATEOAS

Why?

HTTP/1.1 200 OK
Content-Type: application/json

{
    "name": "john",
    "_links": {
        "friends" : "http://user-new-api.com/5/friends/?dev=1"
    }
}
GET /user/5
Host: foo.com
Accept: application/json

No need to agree/argue
with the frontend guy

$http.get(userLink).then(function (user) {

    var link = user.links.frends;

    $http.get(link).then(function(friends) {

        // do something with user friends

    });

});

No need to agree/argue
with the frontend guy

HTTP/1.1 200 OK
Content-Type: application/json

{
    "messages":"http://foo.com/user/{id}/messages",
}
HTTP/1.1 200 OK
Content-Type: application/json

{
    "messages":"http://foo.com/PROXY/user/{id}/messages",
}

URLs can Change

No changes needed on the client

PUT /user/544
Content-Type: application/json

{
    "name":"mike",
}
HTTP/1.1 201 Created
Location: http://foo.com/users/1

Your architecture can change

HTTP/1.1 201 Created
Location: http://foo.com/new-users-db/1
HTTP/1.1 200 OK
Content-Type: application/json

{
    "messages":"http://foo.com/user/{id}/messages",
}
HTTP/1.1 200 OK
Content-Type: application/json

{
    "messages":"http://api-eu.foo.com/user/{id}/messages",
}

Load Balancing

HTTP/1.1 200 OK
Content-Type: application/json

{
    "messages":"http://api-us.foo.com/user/{id}/messages",
}

HATEOAS

Consumers of your API are able to follow the changes of your design

 

(no new code needed)

How to do it?

With PHP?

Server

Framework / Library

  • Symfony?
  • Laravel?
  • Silex?
  • Zend?

Framework / Library

It does not matter!

You will have to do it well, frameworks are just "fancy hammers" (tools)

Of course some frameworks have tools that can help you to implement it faster

Cache, URLs, Authentication and Content Negotiation
 are often framework dependent

Uniform Resource

willdurand/hateoas
+
jms/serializer

For different representation and HATEOAS

/**
 *  @Hateoas\Relation("add", @Hateoas\Route("user_put"))
 *  @Hateoas\Relation("get", @Hateoas\Route("user_get",
 *          parameters = {
 *              "id" = "{id}"
 *          }
 *  ))
 */
class User
{
    /**
     * @var int
     * @Serializer\Exclude
     */
    private $id;
    /**
     * @var string
     * @Serializer\Expose
     */
    private $name;
    /**
     * @var \DateTime
     * @Serializer\Type("DateTime<'Y-m-d H:i:s'>")
     */
    private $registered;
    /**
     * @var boolean
     * @Serializer\Expose
     */
    private $isAdmin = false;  
}
{
    "id": 60,
    "name": "mike",
    "registered": "2010-01-01 20:30:50",
    "admin": "false" ,  
    "_links": {
        "add": "http://foo.com/user/add",
        "get": "http://foo.com/user/{id}"
    } 
}

Client

Client

guzzlehttp/guzzle 
(or php-http/httplug)
 + 
jms/serializer
+
rize/uri-template 

+

guzzlehttp/cache-subscriber
or
kevinrob/guzzle-cache-middleware

REST User Repo

class UserRepository
{
    public function __construct(Client $client, SerializerInterface $serializer) {
        $this->client = $client;
        $this->serializer = $serializer;
    }

    public function getById($id) {
        $uri = $this->resolveLink('user', ['id' => $id]);
        $response = $this->client->get($uri);

        return $this->serializer->deserialize($response->getBody(), User::class, 'json');
    }

    protected function resolveLink($name, $params = []) {
        // get the entry-point index
        $response = $this->client->get();    
        $index = $this->serializer->deserialize($response->getBody(), 'array', 'json');

        // resolve the HATEOAS link
        $expander = new UriTemplate();
        return $expander->expand($index[$name]['href'], $params);
    }
}

Get User

// obtain $client, $serializer
// via some dependency injection container

$userRepo = new UserRepository($client, $serializer);

$user = $userRepo->getById(5);

Add User

class UserRepository {
    ...

    public function create(User $user)
    {
        $json = $serializer->serailze($user, 'json');

        $this->client->put($this->resolveLink('add', ['id' => 'new']), [
            'json' => $json
        ]);
    }
}
// obtain $client, $serializer
// via some dependency injection container

$userRepo = new UserRepository($client, $serializer);

$user = new User('mike');

$userRepo->create($user);

Delete User

class UserRepository {
    ...
    public function delete($id)
    {
        $this->client->delete($this->resolveLink('delete', ['id' => $id]));
    }
}
// obtain $client, $serializer
// via some dependency injection container

$userRepo = new UserRepository($client, $serializer);

// obtain the $user from somewhere

$userRepo->delete($user->getId());

Behind the scene

  • HATEOAS links are resolved
  • Links are followed
  • Resources are serialized/deserialized
  • HTTP cache is used
    • If the resource is in cache
      • no requests are performed
      • cached version is returned
    • If the resource is not in cache
      • ​is fetched and cached
    • Each POST, PUT, DELETE, PATCH clears the URI cache

Embrace standards

Rarely we do something really new

(and if we do, we should always look around first)

no exceptions!

Being standard,
all the pieces of software  we have
glues together with almost no effort

 

 

== less effort == lower costs and risks

Thank you!

If you have questions feel free to ask me now or later! :)

How to get the BEST from your REST (API)

By Asmir Mustafic

How to get the BEST from your REST (API)

  • 1,693