How to get the BEST
from your REST
Asmir Mustafic
(API)
WHO AM I?
Asmir Mustafic
- Twitter: @goetas_asmir
- Github: @goetas
- LinkedIn: @goetas
- WWW: goetas.com
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?
-
Theory about REST API
-
what, how and why
-
-
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
- Client - Server
- Stateless
- Cacheable
- Layered
- 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
- Client - Server
- Stateless
- Cacheable
- Layered
- 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
- Resource = URI
- Different representation have the same URI
- Resources are altered using resource representations
- use the right "verb" to alter resources
- different representation for different proposes
- Self descriptive representations
- how to process the representation
- 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
- If the resource is in 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