Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building REST API with Symfony: experience, errors and recipes

Roma
November 14, 2015

Building REST API with Symfony: experience, errors and recipes

Ever built a REST API? And did you try to do it with Symfony? I will tell the story about a small outsource team, which did all that within several projects. From handling request to generating proper responses. Through versioning, error handling, access and rights control, testing, documenting and more. With examples of our bad decisions and recipes to good ones. This talk will give you the perspective on how to use powerful tools in both good and bad ways with examples from real projects.

Roma

November 14, 2015
Tweet

More Decks by Roma

Other Decks in Programming

Transcript

  1. Building REST API with
    Symfony: experience,
    errors and recipes
    Roman Lapin, Evercode Lab
    [email protected], @memphys

    View Slide

  2. whoami
    Co-founder and CEO
    at Evercode Lab.
    Developer
    transformed into
    manager, occasional
    public speaker and
    consultant and table-
    foosball fan.

    View Slide

  3. Intro

    View Slide

  4. What this talk is NOT about
    Ideology
    Terminology
    Safe/Unsafe
    Idempotency
    RFC2616
    PUT or PATCH
    How to use PATCH
    HTTP Cache
    Unicorns
    Religion

    View Slide

  5. What this talk is NOT about
    Ideology
    Terminology
    Safe/Unsafe
    Idempotency
    RFC2616
    PUT or PATCH
    How to use PATCH
    HTTP Cache
    Unicorns
    Religion

    View Slide

  6. What this talk IS about…
    Practical aspects of building RESTful APIs using
    Symfony with some examples.

    View Slide

  7. …and why?

    View Slide

  8. …and why?
    Because despite of great components, bundles and
    documentation in Symfony world there are too many
    overcomplications and not enough recipes for
    RESTful APIs.

    View Slide

  9. …and why?
    Because despite of great components, bundles and
    documentation in Symfony world there are too many
    overcomplications and not enough recipes for
    RESTful APIs.

    View Slide

  10. …and why?
    Because despite of great components, bundles and
    documentation in Symfony world there are too many
    overcomplications and not enough recipes for
    RESTful APIs.
    And REST itself is not that easy.

    View Slide

  11. Richardson model
    http://martinfowler.com/articles/richardsonMaturityModel.html

    View Slide

  12. –said someone
    “REST has a lot of rules to break”

    View Slide

  13. Who are the users/clients of
    our APIs?
    Mobile applications (iOS, Android etc.)
    Frontend Applications (Angular, Backbone etc.)
    Most of our APIs aren’t open to everyone.

    View Slide

  14. Why Symfony for
    RESTful API

    View Slide

  15. Because it knows about
    HTTP verbs
    Request/Response structures
    Validation
    Security
    and all kinds of other stuff

    View Slide

  16. Because bundles
    FOSRestBundle
    JMSSerializerBundle
    NelmioApiDocBundle

    View Slide

  17. Project structure

    View Slide

  18. ʦʒʒ AppBundle
    ʮʒʒ Admin
    ʮʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web
    ʮʒʒ Dto
    ʮʒʒ Entity
    ʔ ʦʒʒ Repository
    ʮʒʒ Resources
    ʔ ʦʒʒ config
    ʮʒʒ Security
    ʮʒʒ Services
    ʔ ʦʒʒ Listeners
    ʮʒʒ Test
    ʮʒʒ Tests
    ʔ ʦʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web

    View Slide

  19. ʦʒʒ AppBundle
    ʮʒʒ Admin
    ʮʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web
    ʮʒʒ Dto
    ʮʒʒ Entity
    ʔ ʦʒʒ Repository
    ʮʒʒ Resources
    ʔ ʦʒʒ config
    ʮʒʒ Security
    ʮʒʒ Services
    ʔ ʦʒʒ Listeners
    ʮʒʒ Test
    ʮʒʒ Tests
    ʔ ʦʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web

    View Slide

  20. ʦʒʒ AppBundle
    ʮʒʒ Admin
    ʮʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web
    ʮʒʒ Dto
    ʮʒʒ Entity
    ʔ ʦʒʒ Repository
    ʮʒʒ Resources
    ʔ ʦʒʒ config
    ʮʒʒ Security
    ʮʒʒ Services
    ʔ ʦʒʒ Listeners
    ʮʒʒ Test
    ʮʒʒ Tests
    ʔ ʦʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web
    One bundle with division for specific
    classes like controllers, listeners,
    tests, etc.

    View Slide

  21. ʦʒʒ AppBundle
    ʮʒʒ Admin
    ʮʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web
    ʮʒʒ Dto
    ʮʒʒ Entity
    ʔ ʦʒʒ Repository
    ʮʒʒ Resources
    ʔ ʦʒʒ config
    ʮʒʒ Security
    ʮʒʒ Services
    ʔ ʦʒʒ Listeners
    ʮʒʒ Test
    ʮʒʒ Tests
    ʔ ʦʒʒ Controller
    ʔ ʮʒʒ Api
    ʔ ʦʒʒ Web
    One bundle with division for specific
    classes like controllers, listeners,
    tests, etc.

    View Slide

  22. ʮʒʒ Api
    ʔ ʮʒʒ Cart
    ʔ ʔ ʮʒʒ Adapters
    ʔ ʔ ʮʒʒ ChoiceLists
    ʔ ʔ ʦʒʒ Dto
    ʔ ʮʒʒ Exception
    ʔ ʮʒʒ Notifications
    ʔ ʦʒʒ User
    ʔ ʮʒʒ Adapters
    ʔ ʮʒʒ Dto
    ʔ ʮʒʒ Event
    ʔ ʦʒʒ Exception
    ʮʒʒ ApiBundle
    ʔ ʮʒʒ Controller
    ʔ ʮʒʒ Form
    ʔ ʔ ʦʒʒ Subscribers
    ʔ ʮʒʒ Handler
    ʔ ʮʒʒ Listener
    ʔ ʮʒʒ Tests
    ʔ ʦʒʒ Validator
    ʔ ʦʒʒ Constraints
    ʮʒʒ App
    ʔ ʦʒʒ Entity
    ʔ ʮʒʒ Exception
    ʔ ʦʒʒ Repository
    ʦʒʒ AppBundle

    View Slide

  23. ʮʒʒ Api
    ʔ ʮʒʒ Cart
    ʔ ʔ ʮʒʒ Adapters
    ʔ ʔ ʮʒʒ ChoiceLists
    ʔ ʔ ʦʒʒ Dto
    ʔ ʮʒʒ Exception
    ʔ ʮʒʒ Notifications
    ʔ ʦʒʒ User
    ʔ ʮʒʒ Adapters
    ʔ ʮʒʒ Dto
    ʔ ʮʒʒ Event
    ʔ ʦʒʒ Exception
    ʮʒʒ ApiBundle
    ʔ ʮʒʒ Controller
    ʔ ʮʒʒ Form
    ʔ ʔ ʦʒʒ Subscribers
    ʔ ʮʒʒ Handler
    ʔ ʮʒʒ Listener
    ʔ ʮʒʒ Tests
    ʔ ʦʒʒ Validator
    ʔ ʦʒʒ Constraints
    ʮʒʒ App
    ʔ ʦʒʒ Entity
    ʔ ʮʒʒ Exception
    ʔ ʦʒʒ Repository
    ʦʒʒ AppBundle
    Better isolation, but harder to find
    things and more complex structure
    to support.

    View Slide

  24. Handling requests

    View Slide

  25. Separate endpoint
    In the browser:
    /categories
    /orders
    In the API:
    /api/v1/categories
    /api/v1/orders

    View Slide

  26. json or what?
    Support of multiple formats on input and output is cool
    Symfony and FOSRestBundle allow to do it
    But it always adds complexity
    Do you really need it?

    View Slide

  27. json or what?
    fos_rest:
    format_listener: false
    view:
    view_response_listener: force
    formats:
    xml: false
    json: true
    templating_formats:
    html: false
    body_listener:
    decoders:
    json: fos_rest.decoder.jsontoform

    View Slide

  28. FOSRestBundle
    Allows to send payload in json and process it like
    forms (Request params, actually)
    Output data in any set of formats you really need
    Integrates with JMSSerializerBundle to do it easily

    View Slide

  29. Forms
    $form = $this->createForm(new OrderType());
    $form->handleRequest($request);
    if (!$form->isValid()) {
    return $this->returnBadRequest(new
    InputFormatError($this->gatherFormErrors($form)));
    }

    View Slide

  30. Forms
    $form = $this->createForm(new OrderType());
    $form->handleRequest($request);
    if (!$form->isValid()) {
    return $this->returnBadRequest(new
    InputFormatError($this->gatherFormErrors($form)));
    }

    View Slide

  31. Forms
    abstract class AbstractRestApiType extends AbstractType
    {
    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults([
    'csrf_protection' => false,
    'allow_extra_fields' => true
    ]);
    }
    public function getName()
    {
    return '';
    }
    }

    View Slide

  32. Forms
    abstract class AbstractRestApiType extends AbstractType
    {
    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults([
    'csrf_protection' => false,
    'allow_extra_fields' => true
    ]);
    }
    public function getName()
    {
    return '';
    }
    }

    View Slide

  33. Forms
    abstract class AbstractRestApiType extends AbstractType
    {
    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults([
    'csrf_protection' => false,
    'allow_extra_fields' => true
    ]);
    }
    public function getName()
    {
    return '';
    }
    }

    View Slide

  34. ParamFetcher
    fos_rest:
    param_fetcher_listener: true

    View Slide

  35. ParamFetcher
    $title = $paramFetcher->get(‘title’);
    $venue = $this->get(‘app.venue.manager')
    ->create($paramFetcher->all(), $user);
    // Force validation and in the service without forms

    View Slide

  36. Validation and errors

    View Slide

  37. Validator component
    namespace App\Entity;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    class Promocode extends Entity
    {
    /**
    * @ORM\Column(type="string", length=60, nullable=false)
    * @Assert\NotBlank()
    * @var string
    */
    private $code;

    View Slide

  38. Gathering errors
    protected function gatherFormErrors(Form $form)
    {
    $errors = [];
    foreach ($form->getErrors(true) as $err) {
    $errors[$err->getOrigin()->getName()][] =
    $err->getMessage();
    }
    return $errors;
    }

    View Slide

  39. Sending responses

    View Slide

  40. Building blocks
    JMSSerializerBundle
    Model is not always directly a representation
    Use DTO to control response

    View Slide

  41. namespace Api\Address\Dto;
    use JMS\Serializer\Annotation as JMS;
    use Api\Search\Dto\Location;
    class Address
    {
    /**
    * Id of address
    * @JMS\Type("integer")
    * @var int
    */
    public $id;
    /**
    * City
    * @JMS\Type("string")
    * @var string
    */
    public $city;
    /**
    * Location
    * @JMS\Type("Api\Search\Dto\Location")
    * @var Location
    */
    public $location;

    View Slide

  42. public function getUserAddresses(User $user)
    {
    $addressesAsDto = [];
    foreach ($user->getAddresses() as $address) {
    $addressesAsDto[] = $this->adapterFactory
    ->userAddressAdapter()
    ->transform($address);
    }
    return $addressesAsDto;
    }

    View Slide

  43. Why?
    Less magic, more control
    Custom serialization rules and virtual properties
    Isolate entities from response (safe changes)
    Not so bloated entities
    Ease up documentation

    View Slide

  44. Errors structure

    View Slide

  45. Pretty facts about errors

    View Slide

  46. Pretty facts about errors
    How you handle and output errors is one of the most
    important parts of your API.

    View Slide

  47. Pretty facts about errors
    How you handle and output errors is one of the most
    important parts of your API.
    And let’s be honest in most cases it is the last thing
    you pay attention to.

    View Slide

  48. Silver bullet?
    HTTP/1.1 403 Forbidden
    Content-Type: application/problem+json
    Content-Language: en
    {
    "type": "http://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "http://example.net/account/12345/msgs/abc",
    "balance": 30,
    "accounts": ["http://example.net/account/12345",
    "http://example.net/account/67890"]
    }
    Thanks, KnpUniversity!

    View Slide

  49. class ApiProblem
    {
    const TYPE_VALIDATION_ERROR = ‘validation_error';
    const TYPE_INVALID_REQUEST_BODY_FORMAT = 'invalid_body_format';

    private static $titles = array(

    );
    private $statusCode;
    private $type;
    private $extraData = array();
    public function toArray() {
    return array_merge(
    $this->extraData, [
    'status' => $this->statusCode,
    'type' => $this->type,
    'title' => $this->title,
    ]);
    }

    View Slide

  50. class ApiProblemException extends HttpException
    {
    private $apiProblem;
    public function __construct(ApiProblem $apiProblem,
    \Exception $previous = null, array $headers = array(), {
    $this->apiProblem = $apiProblem;
    $statusCode = $apiProblem->getStatusCode();
    $message = $apiProblem->getTitle();
    parent::__construct(
    $statusCode,
    $message,
    $previous,
    $headers,
    $code
    ); }
    }
    }

    View Slide

  51. Then what?
    $apiProblem = new ApiProblem(
    400, ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT
    );
    throw new ApiProblemException($apiProblem);

    View Slide

  52. Then what?
    The ApiProblem object knows everything about how
    the response should look
    You extend and throw ApiProblemException for all the
    problems you get
    You need a common place to catch them and
    transform into a proper response

    View Slide

  53. onKernelException
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
    $exception = $event->getException();
    if ($exception instanceof BadRequestHttpException) {
    }
    if ($exception instanceof ApiProblemException) {
    $response = $exception->getApiProblem()->toArray();
    }
    if ($exception instanceof AccessDeniedHttpException) {
    }
    $event->setResponse($response);
    }

    View Slide

  54. onKernelException
    Limit to api routes only
    Add debug option to show more information in dev
    Catch and process other HttpExceptions you need

    View Slide

  55. Documentation
    NelmioApiDocBundle

    View Slide

  56. NelmioApiDocBundle
    /**
    * @ApiDoc(
    * section="Catalog",
    * resource=true,
    * description="Returns list of places depending on location and
    preferred delivery time",
    * output="Api\Search\Dto\SearchResult",
    * statusCodes={
    * 200="Returned when one or more places found or nothing found",
    * }
    * )
    */
    public function searchCatalogAction(Request $request)

    View Slide

  57. DTO for output
    /**
    * @ApiDoc(
    * section="Catalog",
    * resource=true,
    * description="Returns list of places depending on location and
    preferred delivery time",
    * output="Api\Search\Dto\SearchResult",
    * statusCodes={
    * 200="Returned when one or more places found or nothing found",
    * }
    * )
    */
    public function searchCatalogAction(Request $request)

    View Slide

  58. Forms for input
    /**
    * @ApiDoc(
    * section="Orders",
    * resource=true,
    * input="ApiBundle\Form\CreateFirstOrderType",
    * output="Api\Order\CreatedOrder",
    * description="Creates address and order and returns order number",
    * statusCodes={
    * 200="Returned when order created successfully",
    * 400="Returned when input parameters are invalid"
    * },
    * )
    * @Rest\Post(
    * "/orders/first"
    * )
    */
    public function createFirstOrderAction(Request $request)

    View Slide

  59. QueryParam for filterinng
    * @Rest\Get(
    * "/catalog/search"
    * )
    * @Rest\QueryParam(
    * name="latitude",
    * requirements="-?\d+(\.\d+)?",
    * strict=true,
    * description="Destination latitude"
    * )
    * @Rest\QueryParam(
    * name="longitude",
    * requirements="-?\d+(\.\d+)?",
    * strict=true,
    * description="Destination longitude"
    * )

    View Slide

  60. View Slide

  61. View Slide

  62. View Slide

  63. Versioning

    View Slide

  64. Choose wisely
    URI: /api/v1/cart
    Content-type: application/vnd.app.cart-v1+xml
    Accept: application/vnd.app.cart+xml;v=1

    View Slide

  65. Choose wisely
    URI: /api/v1/cart
    Content-type: application/vnd.app.cart-v1+xml
    Accept: application/vnd.app.cart+xml;v=1

    View Slide

  66. URI versioning
    cart:
    type: rest
    resource: ApiBundle\Controller\CartController
    name_prefix: api_v1_
    prefix: /api/v1

    View Slide

  67. Final thoughts

    View Slide

  68. Security
    Use any mechanism you like to ensure user access
    (token, oAuth, etc.)
    Check the access based on blacklist/whitelist of
    routes in the listener
    Use Voters to check for other complicated access
    rules

    View Slide

  69. Just be consistent in
    Request/Response format
    Errors structure
    Naming stuff (like resources and fields)
    Status codes

    View Slide

  70. naming resources
    /api/v1/admins
    /api/v1/admins/{admin}
    /api/v1/avatars
    /api/v1/categories
    /api/v1/employees
    /api/v1/employees/{id}
    /api/v1/rate

    View Slide

  71. You should read it
    http://williamdurand.fr/2012/08/02/rest-apis-with-
    symfony2-the-right-way/
    http://welcometothebundle.com/symfony2-rest-api-
    the-best-2013-way/
    https://www.ietf.org/rfc/rfc2616.txt

    View Slide

  72. You should watch this
    http://williamdurand.fr/2015/06/02/video-nobody-
    understands-rest/
    https://knpuniversity.com/tracks/rest
    RESTful APIs in the Real World
    Symfony RESTful API

    View Slide

  73. You should thank them
    https://twitter.com/couac
    https://twitter.com/lsmith
    https://twitter.com/weaverryan
    All the creators of and contributors to Symfony, all the
    libraries and bundles around it

    View Slide

  74. Thanks!
    For all the feedback either good or bad use:
    @memphys
    [email protected]

    View Slide

  75. Thanks!
    For all the feedback either good or bad use:
    @memphys
    [email protected]

    View Slide