API Platform

API Platform logo
api-platform.com

Kévin Dunglas

@dunglas

Kévin Dunglas

Les-Tilleuls.coop

@coopTilleuls

  • Self-managed IT company (SCOP)
  • Expertise and dev
  • Symfony, Angular, Node, Go...
  • We are hiring!
Les-Tilleuls.coop

The web has changed!

APIs: the hearth of the new web

  • Central point to access data
  • Encapsulate business logic
  • Same data and same features for desktops, mobiles, customers, providers and partners
Consume Schema

API Platform

A framework for the new web

The big picture

Two separate components

(at least)

  • A static HTML5 webapp (SPA)
  • A web API

Play well with microservices too.

The web API

  • Centralizes R/W access to data
  • Holds all the business logic
  • Is built with PHP (and API Platform)
  • Is stateless (PHP sessions make horizontal scalability harder)

The HTML5 webapp

Or any other client (mobile app...)

  • Holds all the presentation logic
  • Is downloaded first (Single Page Application)
  • Queries the API to retrieve and modify data using AJAX
  • Is 100% composed of HTML, JavaScript and CSS assets
  • Can be hosted on a CDN

The API and the web app are standalone

  • 2 Git repositories and 2 CI
  • different servers
  • 2 domain names: example.com, api.example.com
  • The routing is done client-side using HTML5 push state

Not mandatory for small apps.

Immediate benefits

Speed (even on mobile)

  • Assets including index.html are downloaded from a CDN
  • After the first page load: no more download/parse/render bunch of HTML required at each request
  • Only small chunks of raw data transit on the network
  • API responses can be cached by the proxy

Scalability and robustness

  • The front app is just a set of static files: can be hosted in a CDN
  • Stateless API: push and pop servers/containers on demand

Development comfort

  • A wizard, an autocomplete box? Do it in JS, no more Symfony forms! (API data validation required)
  • Reuse the API in other contexts: native mobile, game stations, TV apps, connected devices or heavy desktop clients
  • Give access to your customers and partners to access to raw data through the API

Long term benefits

  • Better structure: thanks to the central API, less business logic duplication when the app grows
  • Easier refactoring: Touching a component has no impact on the other (spec and test the API format)
  • Simpler project management: Separate teams can work on each app

Drawbacks

SEO and SMO... but solutions exist!

Formats, (open) standards, patterns

HTTP + REST + JSON

  • Work everywhere
  • Lightweight
  • Stateless
  • HTTP has a powerful caching model
  • Extensible (JSON-LD, Hydra...)
  • High quality tooling

JSON Web Token (JWT)

  • Lightweight and simple authentication system
  • Stateless: token signed and verified server-side then stored client-side and sent with each request in an Authorization header
  • Store the token in the browser local storage

HATEOAS / Linked Data

Hypermedia as the Engine of Application State

  • Hypermedia: IRI as identifier
  • Ability to reference external data (like hypertext links)
  • Generic clients

JSON-LD

JSON for Linked Data

  • Standard: W3C recommandation (since 2014)
  • Easy to use: looks like a typical JSON document
  • Already used by Gmail, GitHub, BBC, Microsoft, US gov...
  • Compliant with technologies of the semantic web: RDF, SPARQL, triple store...
{
    "@context": "/contexts/Person",
    "@id": "/people",
    "@type": "hydra:PagedCollection",
    "hydra:totalItems": 1,
    "hydra:itemsPerPage": 30,
    "hydra:firstPage": "/people",
    "hydra:lastPage": "/people",
    "hydra:member": [
        {
        "@id": "/people/1",
        "@type": "http://schema.org/Person",
        "gender": "male",
        "name": "Dunglas",
        "url": "https://dunglas.fr"
        }
    ]
}

Schema.org

  • Define a large set of elements: people, creative work, events, products, chemicals...
  • Created and understood by Google, Bing, Yahoo! et Yandex
  • Massively used, and run by the W3C (Web schemas group)
  • Can be used in HTML (microdata), RDF (RDFa) and JSON-LD
  • Can be extended (custom vocabularies)

Hydra

  • Describe REST APIs in JSON-LD
  • = write support
  • = auto-discoverable APIs
  • = standard for collections, paginations, errors, filters
  • Draft W3C (Work In Progress)

API Platform: the promise

  • Data model generator using Schema.org
  • Fully featured API supporting JSON-LD/Hydra in minutes
  • An autogenerated doc
  • Convenient API spec and test tools using Behat
  • Easy authentication management with JWT or OAuth
  • CORS and HTTP cache
  • All the tools you love: Doctrine ORM, Monolog, Swiftmailer...

Getting started

composer create-project api-platform/api-platform my-api

API Platform <3 Symfony

  • Built on top of Symfony full-stack (support 3.0)
  • Install any existing SF bundles
  • Follow SF Best Practices
  • Can be used in your existing SF app
  • (Optional) tightly integrated with Doctrine

Boostrap a data model with

the schema generator

Step 1

Pick types and properties you need from Schema.org:
namespaces:
  entity: AppBundle\Entity
types:
  Person:
    parent: false
    properties:
      name: ~
      birthDate: ~
      gender: ~

Step 2

Generate

bin/schema generate-types src/ app/config/schema.yml

There is no step 3

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Dunglas\ApiBundle\Annotation\Iri;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * A person (alive, dead, undead, or fictional).
 *
 * @see http://schema.org/Person Documentation on Schema.org
 *
 * @ORM\Entity
 * @Iri("http://schema.org/Person")
 */
class Person
{
    /**
     * @var integer
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @var string The name of the item.
     *
     * @ORM\Column(nullable=true)
     * @Assert\Type(type="string")
     * @Iri("https://schema.org/name")
     */
    private $name;
    /**
     * @var \DateTime Date of birth.
     * @Assert\Date
     * @ORM\Column(type="date", nullable=true)
     * @Iri("http://schema.org/birthDate")
     */
    private $birthDate;
    /**
     * @var string Gender of the person.
     * @Assert\Type(type="string")
     * @ORM\Column(nullable=true)
     * @Iri("http://schema.org/gender")
     */
    private $gender;

    /**
     * Sets id.
     *
     * @param  integer $id
     * @return $this
     */
    public function setId($id)
    {
        $this->id = $id;

        return $this;
    }

    /**
     * Sets name.
     *
     * @param string $name
     *
     * @return $this
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Gets name.
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Gets id.
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Sets birthDate.
     *
     * @param  \DateTime $birthDate
     * @return $this
     */
    public function setBirthDate(\DateTime $birthDate)
    {
        $this->birthDate = $birthDate;

        return $this;
    }

    /**
     * Gets birthDate.
     *
     * @return \DateTime
     */
    public function getBirthDate()
    {
        return $this->birthDate;
    }

    /**
     * Sets gender.
     *
     * @param  string $gender
     * @return $this
     */
    public function setGender($gender)
    {
        $this->gender = $gender;

        return $this;
    }

    /**
     * Gets gender.
     *
     * @return string
     */
    public function getGender()
    {
        return $this->gender;
    }
}

Thanks to Schema.org and API Platform you get:

  • PHP classes, properties, getters and setters (PSR compliant)
  • Doctrine ORM mapping (including relations and mapped superclasses)
  • Validation constraints from the Validator component
  • Full PHPDoc extracted from schema human-readable descriptions
  • (optional) PHP interfaces
  • (optional) ResolveTargetEntity Doctrine mappings
  • (optional) JSON-LD IRI annotations (useful for the API bundle)

PHP Schema is a scaffolding tool

Edit the code and add your custom classes, properties, validation constraints, indexes, IRIs...

Expose your data model through an API

the API bundle

github.com/dunglas/DunglasApiBundle

Define resources to expose


# app/config/services.yml

services:
    person_resource:
        parent:    "api.resource"
        arguments: [ "AppBundle\Entity\Person" ]
        tags:      [ { name: "api.resource" } ]
                        

And you got:

  • Full CRUD with support for relations and dates
  • Data validation
  • Collections pagination
  • Error serialization
  • Automatic routes registration
  • Filters on exposed properties
  • Sorting
  • Hypermedia entrypoint
GET /people
{
    "@context": "/contexts/Person",
    "@id": "/people",
    "@type": "hydra:PagedCollection",
    "hydra:totalItems": 1,
    "hydra:itemsPerPage": 30,
    "hydra:firstPage": "/people",
    "hydra:lastPage": "/people",
    "hydra:member": [
        {
        "@id": "/people/1",
        "@type": "http://schema.org/Person",
        "name": "Dunglas",
        "gender": "male",
        "birthDate": "1988-21-01T00:00:00+01:00"
        }
    ]
}

A collection by API Platform (JSON-LD + Hydra output)

How it works?

The API bundle reuse the following metadata to expose and document the JSON-LD/Hydra API:

  • Doctrine ORM
  • Validator constraints
  • Serializer groups
  • PHPDoc
  • Your own (by registering custom metadata loaders)

Full support of JSON-LD, Hydra and Schema.org

Hydra Console An autodiscoverable Linked Data API for free!

User documentation and sandbox

NelmioApiDoc NelmioApiDoc automatically detects and documents exposed resources.

And many more features

  • Powerful event system (ex: send a mail after a POST)
  • Validation groups
  • Serialization groups
  • Embed relations (ex: GET products and its related offers)
  • Custom operations and controllers
  • Custom filters and sorting options
  • Content Negotiation (experimental)

CORS with NelmioCorsBundle

  • The webapp and the API have different domain names.
  • CORS headers must be set by the API to let the webapp querying it:
# src/app/config.yml

nelmio_cors:
    defaults:
        allow_origin:  ["https://example.com"]
        allow_methods: ["POST", "PUT", "GET", "DELETE", "OPTIONS"]
        allow_headers: ["content-type", "authorization"]
        max_age:       3600
    paths:
        '^/': ~

Security with LexikJwtAuth[...]Bundle

  • Makes the Symfony form login returning a JWT token instead of setting a cookie (stateless)
  • Allows to use Symfony firewall rules to secure API endpoints
# app/config/security.yml

security:
    # ...
    firewalls:
        login:
            pattern:  ^/login$
            stateless: true
            anonymous: true
            form_login:
                check_path: /login
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            require_previous_session: false

        api:
            pattern:   ^/
            stateless: true
            lexik_jwt: ~

    access_control:
        - { path: ^/, roles: ROLE_ANONYMOUS, methods: [GET] }
        - { path: ^/special, roles: ROLE_USER }
        - { path: ^/, roles: ROLE_ADMIN }

Cache with FOSHttpCache

fos_http_cache:
    cache_control:
        rules:
        -
            match:
                path: ^/content$
            headers:
                cache_control:
                    public: true
                    max_age: 64000
                    etag: true
                    

Specs and tests with Behat

Behat and its Behatch extension make testing and API easy.

# features/put.feature
Scenario: Update a resource
    When I send a "PUT" request to "/people/1" with body:
    """
    {
      "name": "Kevin"
    }
    """
    Then the response status code should be 200
    And the response should be in JSON
    And the header "Content-Type" should be equal to "application/ld+json"
    And the JSON should be equal to:
    """
    {
      "@context": "/contexts/Person",
      "@id": "/people/1",
      "@type": "Person",
      "name": "Kevin",
      "address": null
    }
    """

Tools for the static webapp

  • Angular, React, Ember, Backbone or anything else
  • Restangular
  • Auth0 JWT libraries
  • Yeoman, Gulp or Grunt, Bower, Jasmine, Karma...

A JavaScript client

'use strict';

angular.module('myApp')
    .controller('MainCtrl', function ($scope, Restangular) {
        var peopleApi = Restangular.all('people');

        function loadPeople() {
            peopleApi.getList().then(function (people) {
                $scope.people = people;
            });
        }

        loadPeople();

        $scope.newPerson = {};
        $scope.success = false;
        $scope.errorTitle = false;
        $scope.errorDescription = false;

        $scope.createPerson = function (form) {
            peopleApi.post($scope.newPerson).then(function () {
                loadPeople();

                $scope.success = true;
                $scope.errorTitle = false;
                $scope.errorDescription = false;

                $scope.newPerson = {};
                form.$setPristine();
            }, function (response) {
                $scope.success = false;
                $scope.errorTitle = response.data['hydra:title'];
                $scope.errorDescription = response.data['hydra:description'];
            });
        };
    });

They already use API Platform

Stay tunned

  • API Platform 1.1: The first beta released for #forumPHP
  • Support of other persistence layers: soon!
  • Dynamic admin: soon!
  • API Platform 2.0: Already a work in progress

Thank you

joind.in/15492