William Durand — October 29th, 2016 — Symfony Camp :ua:
REST is the underlying architectural principle of the web.
It is formalized as a set of constraints, described in Roy Fielding's PhD dissertation.
- It is all about resources
- Client uses specific HTTP verbs
- Server uses HTTP status codes
- Content Negotiation
- Service discovery via relations
- Hypermedia formats (e.g. HTML, HAL, JSONAPI)
- Resources are not important, their representations are!
- URI design does not matter (URI template)
- Client's experience is crucial!
- Protocol & application semantics
Hypermedia As The Engine Of Application State
It means that hypermedia should be used to find your
way through the API. It is all about state
transitions.
Your application is just a big state machine.
A resource can be anything, and can have more than one representation. A representation describes resource state.
How the underlying resource should behave under HTTP?
What, specifically, will happen to the
application or resource state?
Some standards have a good protocol-level
semantics but no application-level
semantics (Collection+JSON, Atom).
Some standards define a lot of application-level semantics but no protocol semantics (Microformats, Microdata).
:::small * = as described in Richardson and Amundsen's “RESTful Web APIs” book :::
A profile is defined to not alter the semantics of the
resource representation
itself, but to allow clients to
learn about additional semantics, RFC 6906.
Does not have to be machine-readable but recommended.
- Traditional API documentation
- XMDP (for XHTML documents)
- ALPS (XMDP on steroids)
- Embedded doc. (as in HAL/Siren)
- JSON-LD, JSON Schema
Short answer: no. Long answer: no, not yet.
RESTful API must use hypermedia formats. JSON is
not a hypermedia format,
The REST CookBook.
REST APIs are a myth, i.e. too complex in real life.
99.99% of the RESTful APIs out there aren’t fully
compliant
with Roy Fielding’s conception of
REST, Steve Klabnik.
Well-designed, pragmatic, and future-proof APIs.
(Apiary / Drakov)
(Apiary / Aglio thanks to MSON)
+ Attributes
+ data (object)
+ id: `123` (string, required) - The identifier
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"id": { "type": "string", "description": "The identifier" }
},
"required": [ "id" ]
}
}
}
(ex-Swagger)
- NelmioApiDocBundle (#900)
- A Tool to Convert NelmioApiDocBundle to Swagger PHP by Liip (blog post)
- Jane (JSON Schema) and Jane Open Api (Open API)
- Write documentation first, then code
- Must be under version control
- Test your documentation (build / Dredd)
-
Symfony REST Edition = Great for HTTP++ APIs
-
FOSRestBundle + Hateoas = HAL
-
Fractal, Negotiation, etc.
application/vnd.api+json
{
"data": {
"id": "41c6cf",
"type": "sequence",
"attributes": {
"name": "> Demo 1",
"sequence": "ATCGAATCGGTTTAAAATCGATTCCCGAAAA"
}
},
"links": {
"parent": "/data/27d99b56",
"self": "/data/27d99b56/41c6cf",
"source": "https://example.org/demo-sequence.fasta",
"profile": "/profiles/data/sequence"
}
}
{
"errors": [
{
"code": "404",
"status": "404",
"title": "Not Found",
"detail": "The URL you are trying to reach does not exist.",
"links": {
"about": "https://httpstatuses.com/404",
"profile": "/profiles/errors"
}
}
]
}
PATCH /upload/jobs/27d99b56-9327-4d28-a69c-31229bf971aa
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
// /!\ request payload below
{
"data": {
"id": "27d99b56-9327-4d28-a69c-31229bf971aa",
"type": "upload-jobs",
"attributes": {
"data_type": "sequence",
"file_type": "fasta"
}
}
}
(Yes... I wrote about PATCH'ing correctly)
with the fields
query parameter
GET /data/27d99b56?fields[sequence]=name
Accept: application/vnd.api+json
"data": [
{
"id": "41c6cf",
"type": "sequence",
"attributes": { "name": "> Demo 1" }
},
{
"id": "787ff2",
"type": "sequence",
"attributes": { "name": "> Seq. 2" }
}
]
with the filter
query parameter
GET /data/27d99b56?fields[sequence]=sequence \
&filter={"name": { "$regex": "/demo/", "$options": "i"}}
Accept: application/vnd.api+json
"data": [
{
"id": "41c6cf",
"type": "sequence",
"attributes": {
"sequence": "ATCGAATCGGTTTAAAATCGATTCCCGAAAA"
}
}
]
with the page
query parameter
{
"data": [ ... ],
"links": {
"next": "/data/8f7d5ef1?page[cursor]=57de7ff7&page[limit]=1",
"prev": "/data/8f7d5ef1?page[cursor]=-57de7ff7&page[limit]=1"
},
"meta": {
"page": {
"after": "57de7ff7",
"before": "-57de7ff7",
"count": 3138,
"limit": 1
}
}
}
with the include
query parameter
GET /data/27d99b56?include=ANYTHING
Accept: application/vnd.api+json
{
"data": [ ... ],
"included": [
ANYTHING
]
}
POST /upload
Content-Length: 87
Content-Type: text/csv
Accept: application/vnd.api+json
HTTP/1.1 202 Accepted
Content-Type: application/vnd.api+json
Content-Location: /upload/jobs/27d99b56
{
"data": {
"id": "27d99b56",
"type": "upload-jobs",
"attributes": {
"status": "Processing..."
}
}
}
GET /upload/jobs/27d99b56
Accept: application/vnd.api+json
HTTP/1.1 303 See other
Location: /data/27d99b56
Degraded mode for browsers with the Prefer
header
GET /upload/jobs/27d99b56
Accept: application/vnd.api+json
Prefer: status=201
HTTP/1.1 201 Created
Location: /data/27d99b56
-
JSON API helps writing REST APIs
-
It is OK to take shortcuts (sometimes)
-
JSON schema for (application) semantics
public function testPOST()
{
// ... create Guzzle $client
$request = $client->post('/api/programmers', null, json_encode($data));
$response = $request->send();
$request = $client->post('/api/programmers', null, json_encode($data));
$response = $request->send();
$this->assertEquals(201, $response->getStatusCode());
$this->assertTrue($response->hasHeader('Location'));
$data = json_decode($response->getBody(true), true);
$this->assertArrayHasKey('nickname', $data);
}
::: small Source: https://knpuniversity.com/screencast/rest/testing-phpunit :::
# api/features/programmer.feature
# ...
Scenario: Create a programmer
Given I have the payload:
"""
{
"nickname": "ObjectOrienter",
"avatarNumber" : "2",
"tagLine": "I'm from a test!"
}
"""
When I request "POST /api/programmers"
Then the response status code should be 201
And the "nickname" property should equal "ObjectOrienter"
::: small Source: https://knpuniversity.com/screencast/rest/testing :::
describe("HTTP assertions", function () {
it("should make HTTP assertions easy", function () {
var response = chakram.get("http://httpbin.org/get?test=chakram");
expect(response).to.have.status(200);
expect(response).to.have.header("content-type", "application/json");
expect(response).not.to.be.encoded.with.gzip;
expect(response).to.comprise.of.json({
args: { test: "chakram" }
});
return chakram.wait();
});
});
http://dareid.github.io/chakram/
var frisby = require('frisby');
frisby.create('Get Brightbit Twitter feed')
.get('https://api.twitter.com/1/statuses/user_timeline.json?screen_name=brightbit')
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json')
.expectJSON('0', {
user: {
verified: false,
location: "Oklahoma City, OK",
url: "http://brightb.it"
}
})
.toss();
https://github.com/apiaryio/dredd
You can add this to your `.zshrc`:
jsonapi() {
http "$@" Accept:application/vnd.api+json \
Content-Type:application/vnd.api+json
}
- Very important
- Write it first
- Test it
- I ❤️ JSON API
- It is OK not to be 100% REST compliant
- TEST YOUR API!
- No excuse!
- No excuse!
- No excuse!
I did not talk about :fa-lock: security, but:
API key / Authorization header are a good start
OAuth is not for authentication, OpenID Connect is!
JWT (LexikJWTAuthenticationBundle) is trendy
Check out Auth0 article on Symfony
* :fa-globe: [williamdurand.fr](http://williamdurand.fr) * :fa-github: [github.com/willdurand](https://github.com/willdurand) * :fa-twitter: [@couac](https://twitter.com/couac) {no-bullet}