About REST & Symfony

Maxence POUTORD

RevealJS Tips

About me


GET /speaker/maxpou HTTP/1.1

{
  "name": "Maxence POUTORD",
  "skills": ["Symfony2", "API", "NodeJS", "Software quality"],
  "hobbies": ["motorbike", "cinema (Danish)", "cooking"],
  "job": {
    "job": "web consultant",
    "company": "Conserto"
  }
  "_links": {
    "self": { "href": "maxpou.fr/slides/slides/about-rest/" },
    "blog": { "href": "maxpou.fr" },
    "Twitter": { "href": "twitter.com/_maxpou" },
    "mail": { "href": "maxence.poutord@gmail.com" }
  }
}
      

REST:

Representational State Transfer

REST in a nutshell

REST is NOT a protocol!

REST constraints

  • Client–server: clients and servers are separated
  • Stateless: each request can be treated independently
  • Cache: client can cache response
  • Uniform Interface: client and server share a uniform interface
  • Layered system: scalability (load balancing), security (firewalls)
  • *Code-On-Demand (a.k.a. COD): client request code (flash, java applet, js) from server and execute it!


* COD is the only one optional constraint in REST (because it's reduce visibility & not every API need it)

Resources and representations

Resource: can be anything.
Representation: Describe a resource state

beer:
    id: 42
    shortName: Kwak
    name: Pauwel Kwak
    alcoholDegree: 8.4
    color: amber
    since: 1980
    brewery: Bosteels Brewery


  Kwak
  Pauwel Kwak
  8,4
  
    Bosteels Brewery
  

REST and the Richardson Maturity Model (RMM)


martinfowler.com

Level 0: Swamp of POX

  • HTTP tunnel request
  • Only POST/GET method
  • One entry point
    • /beer
    • /beer?action=list
    • /beer?action=get&id=3
    • /beer?action=delete&id=3
  • Example: SOAP and XML-RPC

Level 1: Resources

  • Identification of resources
  • 1 URI = 1 resource
  • Example:
    • /breweries/51
    • /breweries/51/beers/42

Route pattern (1/2):
/beer/1 or /beers/1?

  • Both of them are REST!
  • /hellomynameismaxenceandherearemybeers/1 works too!
  • But, plural noun should be used for collection names

Route pattern (2/2): advices

Avoid:


/beers/42/
/beers/42.json
/beers//limit/26
/ugly_name/42
/UglyName/42
      

Instead, prefer:


/ugly-name/42
/beers?color=blond
/beers?offset=12&limit=5&exclude=2,3&sort=-alcohol
      
"Cool URIs don't change"
— W3C

Level 2: HTTP verbs

  • Client use HTTP verbs
    Example: GET, POST, PUT et DELETE...
  • Server use HTTP codes.
    Represent a result of an action
    Example: 200, 201, 403, 404, 500...

Focus: HTTP Verbs

MethodSafe?Idempotent?
GETYesYes
HEADYesYes
POSTNoNo
PUTNoYes
PATCHNoNo
DELETENoYes

Safe = cacheable
Idempotent = result independent on the number of executions

Focus: HTTP Codes 101

1XXInformation
2XXSuccess
3XXRedirection
4XXClient Error
5XXServer Error

HTTP Status: the bare minimum

  • 200: OK
  • 201: Created
  • 204: No Content
  • 206: Partial Content
  • 301: Moved Permanently
  • 400: Bad request
  • 404: Not Found
  • 500: Internal Server Error

List of HTTP status codes (Wikipedia)

Still lost?

http status codes map
HTTP Status Codes Map (By Restlet)

Am I RESTful if I'm only level 2 compliant?

Why?


GET /beers


  
    Pauwel Kwak
    8.4
  
  
    Tripel Karmeliet
    8.4
  

          

How can I interact with my Kwak?

What did Roy Fielding said

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. Period. [...]
Please try to adhere to them or choose some other buzzword for your API.
Roy T. FIELDING - REST APIs must be hypertext-driven

Level 3: Hypermedia controls (HATEOAS)

  • Hypertext As The Engine Of The Application State
  • Resources are self-describing/self-documenting (discoverability)
  • Expose state and behavior: "the application state is controlled and stored by the user agent"
  • Use links to describe HOW the service is used, and media types to describe WHAT is expect

Without HATEOAS



  
    
      Kwak
    
    
      Pint
    
  
  
    
      Tripel Karmeliet
    
    
      Half
      Pint
    
  

      

HATEOAS



  
    
      Kwak
      
    
    
      
        Half
        
      
    
  
  
    
      Tripel Karmeliet
      
    
    
      
        Half
        
      
      
        Pint
      
    
  
  
    
    
  

      

Content negotiation

Before:


GET /fr/breweries/51.json
GET /en/breweries/51.xml
...

After:


GET /breweries/51
Accept: application/json, application/xml;q=0.9, text/html;q=0.8,
        text/*;q=0.7, */*;q=0.5
Accept-Language: fr, en;q=0.8, de;q=0.7

REST is the web!

Congratulation!

Well... maybe not!

JSON is not a hypermedia format

HAL (Hypertext Application Language)

HAL+JSON example:

{
    "_links": {
        "self": { "href": "/beers?offset=1&limit=10" },
        "next": { "href": "/beers?offset=11&limit=10" },
        "ea:find": {
            "href": "/beers{?id}",
            "templated": true
        }
    }
}
      

JSON-LD


{
    "@context": "http://schema.org",
    "@type": "Brewery",
    "name": "Bosteels Brewery",
    "url": "http://bestbelgianspecialbeers.be",
    "address": {
        "@type": "PostalAddress",
        "streetAddress": "Kerkstraat 96",
        "addressLocality": "9255 Buggenhout",
        "addressRegion": "Belgium"
    }
}
      

Linked Data

Others

  • Hydra + JSON-LD
  • SIREN
  • ...


Which hypermedia type?
sookocheff.com/post/api/on-choosing-a-hypermedia-format/

Focus

REST = CRUD?


POST /contact

# mail body

Versioning


HTTP/1.1 200 OK
Content-Type: application/json; version=2
Location: /breweries/51
      

Deprecated call?


HTTP/1.1 406 NOT ACCEPTABLE
Content-Type: application/json

["application/json; version=1", "application/json; version=2", "application/xml; version=1"]

Cache

Security

Developer experience

3:30:3 rule (by Ori Pekelman)

  • 3 sec: WHAT
  • 30 sec: WHERE
  • 3 min: HOW

Document APIs

RAML 1.0


/beers:
  /{beerId}:
    get:
      description: Retrieve a specific beer
      responses:
        200:
          body:
            application/json:
             example: |
                {
                  "data": {
                    "id": "42",
                    "title": "Kwak",
                    "description": null,
                    "alcohol": 8.4
                    "_link": {
                      "_self": "http:/api.app.com/beers/42"
                    }
                  },
                  "success": true,
                  "status": 200
                }

Swagger

REST & PHP (Symfony2)

JMS Serializer

In a nutshell:

  • (De-)serialize data of any complexity
  • Support XML, JSON, and YAML
  • Doctrine support
  • Custom exclusion strategies

schmittjoh/serializer

Problem with mapping entities

Design Pattern: Data Transfer Object

DTO pattern

Data Transfer Object - Martin Fowler

FOSRestBundle

  • Toolbox
  • Automatic route generation
  • Content negotiation
  • Exceptions
  • Support (Symfony) Forms
  • API versioning

FriendsOfSymfony/FOSRestBundle

FOSRestBundle: Example


/**
 * Get all Breweries entities
 *
 * @QueryParam(name="offset", requirements="\d+", nullable=true,
 *     description="Offset from which to start listing breweries.")
 * @QueryParam(name="limit", requirements="\d+", nullable=true,
 *     description="How many breweries to return.")
 */
public function getBreweriesAction(ParamFetcher $paramFetcher)
{
    $offset = $paramFetcher->get('offset');
    $limit  = $paramFetcher->get('limit');

    $em        = $this->getDoctrine()->getManager();
    $breweries = $em  ->getRepository('MaxpouBeerBundle:Brewery')
                      ->findBy([], ['name' => 'ASC'], $limit, $offset);

    return $breweries;
}
      

BazingaHateoasBundle

  • implementing representations for HATEOAS REST web service
  • Exclusion strategies
  • Allows to configure links and embedded resources in XML, YAML, PHP, or Annotations

willdurand/BazingaHateoasBundle

BazingaHateoasBundle: code


use Hateoas\Configuration\Annotation as Hateoas;
/**
 * BreweryDTO
 * No business logic here
 * @Hateoas\Relation("self", href = @Hateoas\Route("api_get_brewerie", parameters = {"id" = "expr(object.getId())"}))
 */
class BreweryDTO
{
    private $id;
    private $name;

    /** @return UUID object id **/
    public function getId()
    {
        return $this->id;
    }
}
      

BazingaHateoasBundle: result


GET breweries/6b9f3cfa-cc2c-11e5-8adf-2c4138ad20fa

{
    "id": "6b9f3cfa-cc2c-11e5-8adf-2c4138ad20fa",
    "name": "Bosteels brewery",
    "_links": {
      "self": {
        "href": "/beerApp/web/app_dev.php/api/breweries/6b9f3cfa-cc2c-11e5-8adf-2c4138ad20fa"
      }
    }
  }
      

NelmioApiDocBundle

  • Generate a decent documentation
  • Swagger format
  • Support PHPDoc, JMS Serializer, FOSRestBundle, (SF) Forms
  • Export documentation (JSON, HTML, markdown)
  • Multiple documentation
  • Sandbox !

NelmioApiDocBundle: Example


<?php
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
/**
 * Get all Breweries entities
 *
 * @ApiDoc(
 *  statusCodes={
 *      200="Returned when successful"
 * })
 * @QueryParam(name="offset", requirements="\d+", nullable=true,
 *     description="Offset from which to start listing breweries.")
 * @QueryParam(name="limit", requirements="\d+", nullable=true,
 *     description="How many breweries to return.")
 */
 public function getBreweriesAction(ParamFetcher $paramFetcher)
 {
   //...
 }
      

See also

Bundles:

  • friendsofsymfony/http-cache (Reverses proxies like varnish)
  • nelmio/cors-bundle (CORS)
  • hautelook/TemplatedUriRouter (RFC-6570)
  • dunglas/DunglasApiBundle (JSON-LD)

Examples:

  • gimler/symfony-rest-edition
  • liip/LiipHelloBundle

Conclusion

"REST isn’t what you think it is, and that’s OK"

Pragmatism over theory

Useful resources


By Leonard Richardson, Mike Amundsen, Sam Ruby

By Mark Masse

Further Reading

Thank you

Questions?