Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Intro

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

…and why?

Slide 8

Slide 8 text

…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.

Slide 9

Slide 9 text

…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.

Slide 10

Slide 10 text

…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.

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

Why Symfony for RESTful API

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Because bundles FOSRestBundle JMSSerializerBundle NelmioApiDocBundle …

Slide 17

Slide 17 text

Project structure

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

ʦʒʒ 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.

Slide 21

Slide 21 text

ʦʒʒ 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.

Slide 22

Slide 22 text

ʮʒʒ 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

Slide 23

Slide 23 text

ʮʒʒ 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.

Slide 24

Slide 24 text

Handling requests

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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?

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

ParamFetcher fos_rest: param_fetcher_listener: true

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Validation and errors

Slide 37

Slide 37 text

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;

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Sending responses

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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; …

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Errors structure

Slide 45

Slide 45 text

Pretty facts about errors

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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.

Slide 48

Slide 48 text

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!

Slide 49

Slide 49 text

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, ]); }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Documentation NelmioApiDocBundle

Slide 56

Slide 56 text

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)

Slide 57

Slide 57 text

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)

Slide 58

Slide 58 text

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)

Slide 59

Slide 59 text

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" * )

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

Versioning

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Final thoughts

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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