Skip to content

Latest commit

 

History

History
699 lines (490 loc) · 12.6 KB

monod.md

File metadata and controls

699 lines (490 loc) · 12.6 KB

Pragmatic APIs 101

William Durand — October 29th, 2016 — Symfony Camp :ua:


What's REST?


REpresentational State Transfer

REST is the underlying architectural principle of the web.

It is formalized as a set of constraints, described in Roy Fielding's PhD dissertation.


Richardson Maturity Model


Levels 0, 1, and 2

  • It is all about resources
  • Client uses specific HTTP verbs
  • Server uses HTTP status codes
  • Content Negotiation

Level 3 - Hypermedia Controls

  • 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

HATEOAS

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.


Resource vs Representation

A resource can be anything, and can have more than one representation. A representation describes resource state.


Protocol Semantics

How the underlying resource should behave under HTTP?


Application Semantics

What, specifically, will happen to the
application or resource state?


The Semantic Challenge*

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 :::

Profiles

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.


Examples

  • Traditional API documentation
  • XMDP (for XHTML documents)
  • ALPS (XMDP on steroids)
  • Embedded doc. (as in HAL/Siren)
  • JSON-LD, JSON Schema

State Of The Art Industry


We all want RESTful APIs


Yes.


Is My API RESTful When I Use JSON?

Short answer: no. Long answer: no, not yet.
RESTful API must use hypermedia formats. JSON is
not a hypermedia format
, The REST CookBook
.


No Relations = No REST

If the engine of application state (and hence the API)
is not being driven by hypertext, then it cannot be
RESTful and cannot be a REST API
, Roy Fielding
.


s/REST/HTTP++/

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
.


Are we all screwed and should
we all jump to GraphQL?


Nope.


The Pragmatic Way

Well-designed, pragmatic, and future-proof APIs.

{no-border}


:fa-book: Documentation


API Blueprint
{no-border}


Apiary
{no-border}


Apiary
{no-border}


Aglio
{no-border}


Moooocks!

(Apiary / Drakov)


JSON Schema

(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" ]
    }
  }
}

{no-border}

(ex-Swagger)


Symfony/PHP


Advices

  • Write documentation first, then code
  • Must be under version control
  • Test your documentation (build / Dredd)

:fa-code: Code


Symfony/PHP


{no-border}


{no-border}


JSON API

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

{
  "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"
      }
    }
  ]
}

Update with PUT PATCH

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)


Sparse Fieldsets :fa-heart:

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" }
  }
]

Filtering

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"
    }
  }
]

Pagination

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
    }
  }
}

Inclusion

with the include query parameter

GET /data/27d99b56?include=ANYTHING

Accept: application/vnd.api+json
{
  "data": [ ... ],
  "included": [
    ANYTHING
  ]
}

Asynchronous Processing (1/3)

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..."
    }
  }
}

Asynchronous Processing (2/3)

GET /upload/jobs/27d99b56

Accept: application/vnd.api+json
HTTP/1.1 303 See other
Location: /data/27d99b56

Asynchronous Processing (3/3)

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

Recap'

  • JSON API helps writing REST APIs

  • It is OK to take shortcuts (sometimes)

  • JSON schema for (application) semantics


:fa-check-circle: Testing


PHPUnit + Guzzle

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 :::


Behat

# 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 :::


Chakram

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/


Frisby

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();

http://frisbyjs.com


Dredd (for API doc)

{no-border}


https://github.com/apiaryio/dredd


Postman
{no-border}


Hurl . it
{no-border}


And...

  • HTTPie: cURL-like tool for humans
  • jq: like sed for JSON data

You can add this to your `.zshrc`:
jsonapi() {
  http "$@" Accept:application/vnd.api+json \
            Content-Type:application/vnd.api+json
}

Conclusion


:fa-book: Documentation

  1. Very important
  2. Write it first
  3. Test it

:fa-code: Code

  1. I ❤️ JSON API
  2. It is OK not to be 100% REST compliant
  3. TEST YOUR API!

:fa-check-circle: Testing

  1. No excuse!
  2. No excuse!
  3. No excuse!

One More Thing

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


A: I don't know.
{no-border}


Thank you.

Questions?


* :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}