API Tips from the Frontline

API Tips from the Frontline

Starting to write an API is an easy task, but you quickly stumble upon many obstacles and hard decisions. How to manage result pagination? How to handle write operations and file uploads? Join me as I share my tricks that allowed me to ship high-profile projects in record time, while keeping the code clean and maintainable.

B3b2139e4f2c0eca4efe2379fcebc1c5?s=128

Anna Filina

April 02, 2015
Tweet

Transcript

  1. foolab.ca | @foolabca API Tips From the Frontline PHP Quebec,

    Montreal - April 2, 2015
  2. Anna Filina • Developer • Problem solver • Coach •

    Advisor 2
  3. What you will learn • Don't repeat my mistakes. •

    Overcome common obstacles. • Build elegant APIs. 3
  4. Endpoints What URL to call when.

  5. Lists • /products • /products?category=games,movies&lang=fr • /products?include=platforms.name&sort=-date • /products?page[number]=2&page[size]=100 5

  6. Details • /products/1 • /products/1?include=photos,reviews 6

  7. Write • POST /products • PUT /products/1 • DELETE /products/1

    • Version your APIs: api.example.org/v1/products 7
  8. Request/Response How to format stuff.

  9. Multiple formats • xml, json, etc. • Can use 1

    endpoint with Accept header. • Can send version, pagination & language info using headers too. 9
  10. Request • Bad: Content-Type: application/x-www-form-urlencoded. • Content-Type: application/json. • Send

    content in body, not headers. 10
  11. Request example POST /products HTTP/1.1 Host: api.example.org Content-Type: application/json; charset=UTF-8

    { "data": { "name": "Skyrim", "price": 19.99 } } 11
  12. Response example: list { "data": [ {"name": "Skyrim"}, {"name": "Civilization

    V"}, ], "page": { "number": 1, "size": 2, "pages": 50, "total": 100, } } 12
  13. 3 queries • Count query with same filters but no

    pagination. • Query with filters, pagination & sorting, but only select DISTINCT IDs. ◦ Because JOIN + LIMIT = wrong number of root records. • Query all the data by IDs and apply sorting. 13
  14. Upload • Use tools, don't DIY (do it yourself). •

    Client-side: Plupload • Step 1: upload temp file. • Step 2: give file path to next API request. • Server-side: Guzzle. • Look under $_FILES. 14
  15. Plupload var uploader = new plupload.Uploader({ runtimes: 'html5,html4', max_file_size: '5mb',

    url: '/api/upload', filters: [{extensions: 'jpg,png,jpeg'}] }); uploader.init(); 15
  16. Guzzle <? $url = 'http://example.org/profiles/1/edit'; $request = $client->createRequest('POST', $url); $postBody

    = $request->getBody(); $postBody->setField('data', ['first_name':'Anna']); $file = new PostFile('i.jpg', fopen('/path', 'r')); $postBody->addFile($file); $response = $client->send($request); 16
  17. Status codes • Don't confuse API & HTTP codes. •

    2xx success • 3xx redirect • 4xx client error • 5xx server error • Send API-specific code in body. 17
  18. Example of error Status: 400 Content-Type: application/json; charset=UTF-8 { "error":

    { "code": 1001, "message": "Price must be greater than 0." } } 18
  19. Testing Simpler than you think.

  20. HTTP • Use Guzzle or built-in framework tool. • Generate

    HTTP request , compare output. 20
  21. Guzzle test // tests/ApiProductTest.php public function testGetOneProduct() { $client =

    new Client(); $response = $client->get('http://api.example.org/v1/products/1', [ 'exceptions' => false, 'headers' => ['Accept' => 'application/json'] ]); $this->assertEquals(200, $response->getStatusCode()); $body = $response->getBody()->getContents(); $this->assertJsonStringEqualsJsonString('{ "data": { "id": "1", "name": "Skyrim", "price": 19.99 } }', $body); } 21
  22. Testing tips • Create separate database for tests. • Write

    tests before you code: • TDD • Contract between you and frontend dev. 22
  23. Authentication Nooo, I hate that part! Someone else code it

    plz.
  24. Multiple methods • You can have multiple auth methods for

    one API. • Session can be one of them if same app. 24
  25. OAuth2 • SSL required. • Advanced features like access scope.

    • More security: use authorization server. 25
  26. OAuth2 - Step 1 (simplified approach) 26 Login User Client

    app
 (php/js/mobile) Forward API Validate Create token Store token user/pass user/pass token
  27. OAuth2 - Step 2 27 Fetch Display page Done token

    data page Access page Request data Validate token User Client app
 (php/js/mobile) API Authorization: Bearer zaVOP1lmQja4Lz-^RLwvzapvjd
  28. OAuth2 - Auth server 28 User Client app
 (php/js/mobile) API

    Auth Request Forward Validate Create token Store token token Login form Login Store token token user/pass
  29. Refactoring to API Don't rewrite all the legacy at once.

  30. Micro-services 30 Products Orders Inventory /categories /products /reviews /customers /orders

    /warehouses /sales-reps get info update
 stock get info
  31. Progressive rewrite • Rewrite one component at a time. •

    Start with whatever has fewer dependencies (or critical). • Will have duplicate functionality for a time. ◦ Example: orders will still fetch products the old way. • Copy production data for dev environment. 31
  32. Services vs libraries • Services: GET /products?id=1,5 • Libraries: •

    $db->getList( ["id" => "1,5"] ); • Can fetch without pagination. • Can hydrate to models. • Bypass HTTP. 32
  33. Implementation tips Is this a good way to code it?

  34. Libraries • I use Symfony & Doctrine. • Aside from

    frameworks, I prefer small tools that don't do too much magic. • OptionsResolver component to validate parameters. • Validator to validate entities (models). 34
  35. OptionsResolver $resolver = new OptionsResolver(); $resolver->setDefaults([ 'sort' => null, 'lang'

    => 'en', ]); $resolver->setAllowedTypes('lang','string'); $resolver->setAllowedValues('lang',['en', 'fr']); $resolver->resolve($options); 35
  36. Validator $validator = Validation::createValidator(); $constraint = new Assert\Collection([ 'name' =>

    new Assert\Length(['min'=>2]), 'photos' => new Assert\Collection([ 'file' => new Assert\Image(), ]), ]); $violations = $validator->validateValue($input, $constraint); 36
  37. Validator • Can read constraints from model annotations.
 /** @Assert\

    Length(min=5, minMessage="Too short") **/ • List of violations for you to forward to client. • Can validate files, emails, IPs, credit cards, dates, etc. 37
  38. Reuse & standardize • Goal: streamline endpoint creation. • Base

    controller for common request processing. • Base repository for querying with filters. • Keep things fully customizable. ◦ Not too much magic. ◦ I avoid out-of-the-box solutions. 38
  39. Performance Make things faster. Much faster.

  40. Benchmark • Make performance part of your test suite. •

    Give Blackfire a spin. • In dev/test mode, use a meta block (or add to headers). 40
  41. Example { "meta": { "perf": { "db_time": 0.00367, "total_time": 0.3120,

    "memory": 19661 } } } 41
  42. Perfomance tips • Don't use lazy loading. Example: $product->getPhotos(). •

    Craft your own joins and carefully select fields. • Avoid ORM built-in hydration for read operations. • Use API keys even for public endpoints (DDoS mitigation). • Put stuff in Memcached/Redis (especially blocked keys). 42
  43. Useful links • Standard API format
 http://jsonapi.org/format/ • Blackfire
 https://blackfire.io

    • OAuth2
 http://oauth2.thephpleague.com/ • OAuth2 with mobile
 http://www.slideshare.net/briandavidcampbell/... • Symfony components
 http://symfony.com/doc/current/components/index.html • Book "Build APIs You Won't Hate"
 https://leanpub.com/build-apis-you-wont-hate 43
  44. Anna Filina • @afilina • afilina.com • Development: PHP, JS,

    etc. • Fix problems: bugs, performance, etc. • Workshops: testing, Symfony, AngularJS, API, etc. • Advisor: testing strategy, legacy code, etc. 44
  45. @afilina afilina.com