Asmir Mustafic
Software architect and consultant
(mainly PHP)
Helping companies deal with their tech stack
contributed/contributing to:
Theory about REST API
what, how and why
Some PHP implementations
hints and tricks
no browsers or mobile
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 ...
not easy to achieve all of them!
Roy Fielding (2000)
Separation of responsibilities
The server stores no information about the client,
so the architecture scales well
The client has access to resources between it and the server,
so che communication is faster
Allows intermediary nodes transparently,
for scalability, cacheability, encapsulation, extension...
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...
(almost 20 years ago)
(and no body knew it)
HTTP is definitely client-server
sessions and cookies
are implemented at application level
HTTP is just a transport protocol
almost everybody forget it!
...or invents a way to make it not cacheable
Rich set of Cache headers & policies
ME > application > application-wrapper > webserver > load-balancer > reverse-proxy > firewall > cdn > proxy > local-cache > browser > YOU
Allows encapsulation in other protocols (see HTTP2)
resources, resource identification (URIs), resource types (MIME), operations (verbs)
Personal experience,
often small mistakes are the source of big problems
Do not store any kind of state information
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
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
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
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
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?
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",
...
}
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
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
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?
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}
]
When the user gets a new friend...
$cache->invalidateByTag('user-343');
BAN /
Cache-Tags: user-343
Not yet standardized... but works!
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?
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!
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
(entrypoint)
https://tools.ietf.org/html/rfc6570
(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
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
$http.get(userLink).then(function (user) {
var link = user.links.frends;
$http.get(link).then(function(friends) {
// do something with user friends
});
});
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",
}
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
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",
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"messages":"http://api-us.foo.com/user/{id}/messages",
}
(no new code needed)
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
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}"
}
}
guzzlehttp/guzzle (or php-http/httplug) + jms/serializer + rize/uri-template
+
guzzlehttp/cache-subscriber
or
kevinrob/guzzle-cache-middleware
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);
}
}
// obtain $client, $serializer
// via some dependency injection container
$userRepo = new UserRepository($client, $serializer);
$user = $userRepo->getById(5);
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);
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());
Rarely we do something really new
(and if we do, we should always look around first)
no exceptions!
== less effort == lower costs and risks
If you have questions feel free to ask me now or later! :)