Writing APIs in Lumen

Writing APIs in Lumen

Modern applications increasingly require an API, whether to support rich client-side experiences, mobile apps, or to integrate with other systems, and Lumen is an excellent tool for this job. Lumen is lightweight and focussed on providing stateless, JSON APIs which is ideal. I want your API to be a good HTTP citizen and will show you how to build a really excellent and robust API in Lumen including how to handle core HTTP features, such as media negotiation & status codes. We'll also look providing developer-friendly features like thoughtful error handling and documentation. By the end of this session, you'll know how to build great Lumen APIs that give you a competitive edge, ensuring that developers want to work with it.

79d9ba388d6b6cf4ec7310cad9fa8c8a?s=128

Rob Allen

June 11, 2018
Tweet

Transcript

  1. Writing APIs in Lumen Rob Allen June 2018

  2. I write APIs Rob Allen ~ @akrabat

  3. Let's start with Lumen Rob Allen ~ @akrabat

  4. Lumen • Microframework • Built with Laravel components • Fast!

    Rob Allen ~ @akrabat
  5. Installation Install Laravel Installer: $ composer global require laravel/lumen-installer Create

    a new project: $ lumen new my-project-name Create .env file $ cd my-project-name $ cp .env.example .env Rob Allen ~ @akrabat
  6. Run using local webserver $ php -S 127.0.0.1:8888 -t public/

    $ curl http://localhost:8888 Lumen (5.6.3) (Laravel Components 5.6.*) Rob Allen ~ @akrabat
  7. Differences from Laravel: Artisan Artisan is pared down: • No

    serve • ONLY make:migration • No key:generate • No make:model • No tinker • No make:controller • No env • No make:auth • No down • etc Rob Allen ~ @akrabat
  8. Add more Artisan! 1. Install flipbox's Lumen Generator: $ composer

    require --dev flipbox/lumen-generator 2. Inside bootstrap/app.php file, add: $app->register(Flipbox\LumenGenerator\ LumenGeneratorServiceProvider::class); Rob Allen ~ @akrabat
  9. Lumen-Generator Provides these additional artisan commands: make:command make:controller key:generate make:event

    make:job clear-compiled make:listener make:mail serve make:middleware make:migration tinker make:model make:policy optimize make:provider make:seeder route:list make:test Rob Allen ~ @akrabat
  10. Differences from Laravel: Config • No config/ directory • Configuration

    only via .env APP_ENV=local APP_DEBUG=true APP_KEY= APP_TIMEZONE=UTC LOG_CHANNEL=stack LOG_SLACK_WEBHOOK_URL= … Rob Allen ~ @akrabat
  11. Differences from Laravel: app.php • Register service providers in bootstrap/app.php

    $app->register(App\Providers\AppServiceProvider::class); • Register middleware in bootstrap/app.php $app->middleware([ App\Http\Middleware\ExampleMiddleware::class ]); $app->routeMiddleware([ 'auth' => App\Http\Middleware\Authenticate::class, ]); Rob Allen ~ @akrabat
  12. Differences from Laravel: Routing Laravel (Symfony Router): Route::get("/books/{id?}", function($id =

    null) { // do stuff })->where('id', '[0-9]+'); Lumen (FastRoute): $router->get('/books[/{id:[0-9]+}]', function ($id = null) { // do stuff }); Rob Allen ~ @akrabat
  13. Disabled features bootstrap/app.php: // $app->withFacades(); // $app->withEloquent(); Rob Allen ~

    @akrabat
  14. /ping: An API's "Hello World" routes/web.php: $router->get('/ping', function () {

    return response()->json(['ack' => time()]); }); Rob Allen ~ @akrabat
  15. /ping: An API's "Hello World" routes/web.php: $router->get('/ping', function () {

    return response()->json(['ack' => time()]); }); $ curl -i http://localhost:8888/ping HTTP/1.0 200 OK Host: localhost:8888 Content-Type: application/json {"ack":1527326698} Rob Allen ~ @akrabat
  16. What is an API? Rob Allen ~ @akrabat

  17. What is a web API? A server-side web API is

    a programmatic interface consisting of one or more publicly exposed endpoints to a defined request–response message system, typically expressed in JSON or XML, which is exposed via the web Wikipedia Rob Allen ~ @akrabat
  18. What is a web API? • Programmatic interface • Endpoints

    • Request-response message system • JSON or XML • Stateless Rob Allen ~ @akrabat
  19. What is REST? • An architecture • Centres on the

    transfer of representations of resources • A resource is any concept that can be addressed • A representation is typically a document that captures the current or intended state of a resource • A client makes requests of a server when it wants to transition to a new state Rob Allen ~ @akrabat
  20. Strengths • Loose coupling • Leverages the power of HTTP

    • Emphasis on readability • HTTP methods as verbs: GET, POST, PUT, DELETE, etc. • Resources as nouns: collections and entities Rob Allen ~ @akrabat
  21. Constraints: Client/Server • Clients are not concerned with storage (portable)

    • Servers are not concerned with UI or user state (scalable) Rob Allen ~ @akrabat
  22. Constraints: Stateless • No client context stored between requests. (no

    sessions!) Rob Allen ~ @akrabat
  23. Constraints: Cacheable • Safe idempotent methods are always cacheable •

    Non-idempotent methods should allow clients to cache responses. • Clients should honour HTTP headers with respect to caching. Rob Allen ~ @akrabat
  24. Constraints: Layered system • Client should not care whether it

    is connected directly to the server, or to an intermediary proxy. Rob Allen ~ @akrabat
  25. Constraints: Uniform Interface • Identification of resources • Manipulation of

    resources through representations • Self-descriptive messages • Hypermedia as the engine of application state (HATEOAS) Rob Allen ~ @akrabat
  26. Primary aspects of a RESTful API • URI for each

    resource: https://example.com/authors/1 • HTTP methods are the set of operations allowed for the resource • Media type used for representations of the resource • The API must be hypertext driven Rob Allen ~ @akrabat
  27. HTTP methods Method Used for Idempotent? GET Retrieve data Yes

    PUT Change data Yes DELETE Delete data Yes POST Change data No PATCH Update data No Send 405 Method Not Allowed not available for that resource Rob Allen ~ @akrabat
  28. HTTP method negotiation Lumen provides this for free! $ curl

    -I -X DELETE http://localhost:8888 HTTP/1.0 405 Method Not Allowed Host: localhost:8888 Allow: GET, POST Connection: close Content-type: text/html; charset=UTF-8 Rob Allen ~ @akrabat
  29. Routing in Laravel routes/web.php: $router->get('/authors/{id:\d+}', [ 'as' => 'author.list', 'uses'

    => 'AuthorController@show' ]); Rob Allen ~ @akrabat
  30. Routes have a method Single methods: $router->get() $router->post() $router->put() $router->patch()

    $router->delete() $router->options() Multiple methods: $router->addRoute(['GET', 'POST'], …) Rob Allen ~ @akrabat
  31. Routes have a pattern • Literal string match $router->get('/authors', …);

    Rob Allen ~ @akrabat
  32. Routes have a pattern • Literal string match $router->get('/authors', …);

    • Placeholders are wrapped in { } $router->get('/authors/{id}', …); Rob Allen ~ @akrabat
  33. Routes have a pattern • Literal string match $router->get('/authors', …);

    • Placeholders are wrapped in { } $router->get('/authors/{id}', …); • Optional segments are wrapped with [ ] $router->get('/authors[/{id}[/{books}]]', …); Rob Allen ~ @akrabat
  34. Routes have a pattern • Literal string match $router->get('/authors', …);

    • Placeholders are wrapped in { } $router->get('/authors/{id}', …); • Optional segments are wrapped with [ ] $router->get('/authors[/{id}[/{books}]]', …); • Constrain placeholders via Regex $router->get('/authors/{id:\d+}', …); // digits Rob Allen ~ @akrabat
  35. Routes have a name Use as key to specify a

    name: $router->get('/authors/{id:\d+}', [ 'as' => 'author.list', 'uses' => 'AuthorController@show' ]); Generate URL to named route: $url = route('authors', ['id' => 1]); // generates: /authors/1 Rob Allen ~ @akrabat
  36. Routes have an action Use uses key to specify a

    controller: $router->get('/authors/{id:\d+}', [ 'as' => 'author.list', 'uses' => 'AuthorController@show' ]); Rob Allen ~ @akrabat
  37. Action method in a controller namespace App\Http\Controllers; use …; class

    AuthorController extends Controller { public function show(int $id) { $author = Author::findOrFail($id); return $author; } } Rob Allen ~ @akrabat
  38. Status codes Send the right one for the right situation!

    1xx Informational 2xx Success 3xx Redirection 4xx Client error 5xx Server error Rob Allen ~ @akrabat
  39. Status codes are set in the Response // AuthorController public

    function add(Request $request): Response { $data = $this->validate($request, [ 'name' => 'required|max:100', ]); $author = Author::create($data)->save(); return response()->json($author, 201); } Rob Allen ~ @akrabat
  40. Status codes $ curl -i -d name="Octavia Butler" \ http://localhost:8888/authors

    HTTP/1.0 201 Created Host: localhost:8888 Content-Type: application/json {"name":"Octavia Butler", "updated_at":"2018-05-26 14:55:27", "created_at":"2018-05-26 14:55:27", "id":7} Rob Allen ~ @akrabat
  41. Content negotiation Correctly parse the request • Read the Content-Type

    header • Raise "415 Unsupported media type" status if unsupported Correctly create the response • Read the Accept header • Set the Content-Type header Rob Allen ~ @akrabat
  42. Handling unsupported types class UnsupportedMiddleware { public function handle($request, Closure

    $next) { $type = $request->headers->get('content-type'); if (stripos($type, 'application/json') !== 0) { return response('Unsupported Media Type', 415); } return $next($request); } } Rob Allen ~ @akrabat
  43. Handling unsupported types class UnsupportedMiddleware { public function handle($request, Closure

    $next) { $type = $request->headers->get('content-type'); if (stripos($type, 'application/json') !== 0) { return response('Unsupported Media Type', 415); } return $next($request); } } Rob Allen ~ @akrabat
  44. Handling unsupported types class UnsupportedMiddleware { public function handle($request, Closure

    $next) { $type = $request->headers->get('content-type'); if (stripos($type, 'application/json') !== 0) { return response('Unsupported Media Type', 415); } return $next($request); } } Rob Allen ~ @akrabat
  45. Handling invalid Accept header class UnacceptableMiddleware { public function handle($request,

    Closure $next) { $accept = $request->headers->get('accept'); if ($accept && stripos($accept, 'json') === false) { return response->json(['error' => 'You must accept JSON'], 406); } return $next($request); } } Rob Allen ~ @akrabat
  46. Handling invalid Accept header class UnacceptableMiddleware { public function handle($request,

    Closure $next) { $accept = $request->headers->get('accept'); if ($accept && stripos($accept, 'json') === false) { return response->json(['error' => 'You must accept JSON'], 406); } return $next($request); } } Rob Allen ~ @akrabat
  47. Handling invalid Accept header class UnacceptableMiddleware { public function handle($request,

    Closure $next) { $accept = $request->headers->get('accept'); if ($accept && stripos($accept, 'json') === false) { return response->json(['error' => 'You must accept JSON'], 406); } return $next($request); } } Rob Allen ~ @akrabat
  48. Handling invalid Accept header $ curl -i -H "Accept: application/xml"

    http://localhost/ HTTP/1.0 406 Not Acceptable Content-Type: application/json {"error":"You must accept JSON"} Rob Allen ~ @akrabat
  49. Hypermedia • Media type used for a representation • The

    link relations between representations and/or states • Important for discoverability Rob Allen ~ @akrabat
  50. JSON and Hypermedia JSON does not have a defined way

    of providing hypermedia links Options: • "Link" header (GitHub approach) • application/collection+json • application/hal+json • JSON-API Rob Allen ~ @akrabat
  51. Fractal • Separate the logic for your JSON formation from

    your Eloquent model • Supports multiple serialisers including JSON-API • Install: $ composer require league/fractal Rob Allen ~ @akrabat
  52. Fractal service provider class FractalManagerProvider extends ServiceProvider { public function

    register() { $this->app->singleton(Manager::class,function($app) { $manager = new Manager(); $base = app(Request::class)->getBaseURL(); $manager->setSerializer(new JsonApiSerializer($base)); return $manager; }); } } Rob Allen ~ @akrabat
  53. Fractal service provider class FractalManagerProvider extends ServiceProvider { public function

    register() { $this->app->singleton(Manager::class,function($app) { $manager = new Manager(); $base = app(Request::class)->getBaseURL(); $manager->setSerializer(new JsonApiSerializer($base)); return $manager; }); } } Rob Allen ~ @akrabat
  54. Fractal service provider class FractalManagerProvider extends ServiceProvider { public function

    register() { $this->app->singleton(Manager::class,function($app) { $manager = new Manager(); $base = app(Request::class)->getBaseURL(); $manager->setSerializer(new JsonApiSerializer($base)); return $manager; }); } } Rob Allen ~ @akrabat
  55. Fractal service provider class FractalManagerProvider extends ServiceProvider { public function

    register() { $this->app->singleton(Manager::class,function($app) { $manager = new Manager(); $base = app(Request::class)->getBaseURL(); $manager->setSerializer(new JsonApiSerializer($base)); return $manager; }); } } Rob Allen ~ @akrabat
  56. AuthorTransformer class AuthorTransformer extends Fractal\TransformerAbstract { public function transform(Author $author)

    { return [ 'id' => (int) $author->id, 'name' => $author->name, ]; } } Rob Allen ~ @akrabat
  57. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  58. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  59. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  60. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  61. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  62. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  63. Create JSON-API response public function list(Manager $fractal) : Response {

    $authors = Author::all(); $resource = new Collection($authors, new AuthorTransformer, 'authors'); return response()->json( $fractal->createData($resource)->toArray(), 200, ['content-type' => 'application/vnd.api+json'] ); } Rob Allen ~ @akrabat
  64. Output $ curl http://localhost:8888/authors/4 { "data": [ { "type": "authors",

    "id": "1", "attributes": { "name": "Suzanne Collins", }, "links": { "self": "/authors/1" } }, Rob Allen ~ @akrabat
  65. When things go wrong Rob Allen ~ @akrabat

  66. Default error output $ curl http://localhost:8888/authors/999 <!DOCTYPE html> <html> <head>

    <meta name="robots" content="noindex,nofollow" /> <style> /* Copyright (c) 2010, Yahoo! Inc. All rights reser html{color:#000;background:#FFF;}body,div,dl,dt,dd, html { background: #eee; padding: 10px } img { border: 0; } #sf-resetcontent { width:970px; margin:0 auto; } Rob Allen ~ @akrabat
  67. Great error handling • Error representations are first class citizens

    • Provides application error code & human readable message • Pretty prints for the humans! Rob Allen ~ @akrabat
  68. Override ExceptionsHandler::render() public function render($request, Exception $e) { $statusCode =

    $this->getStatusCodeFromException($e); $error['error'] = Response::$statusTexts[$statusCode]; if (env('APP_DEBUG')) { $error['message'] = $e->getMessage(); $error['file'] = $e->getFile() . ':' . $e->getLine(); $error['trace'] = explode("\n", $e->getTraceAsString()); } return response()->json($error, $statusCode, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); } Rob Allen ~ @akrabat
  69. Override ExceptionsHandler::render() public function render($request, Exception $e) { $statusCode =

    $this->getStatusCodeFromException($e); $error['error'] = Response::$statusTexts[$statusCode]; if (env('APP_DEBUG')) { $error['message'] = $e->getMessage(); $error['file'] = $e->getFile() . ':' . $e->getLine(); $error['trace'] = explode("\n", $e->getTraceAsString()); } return response()->json($error, $statusCode, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); } Rob Allen ~ @akrabat
  70. Override ExceptionsHandler::render() public function render($request, Exception $e) { $statusCode =

    $this->getStatusCodeFromException($e); $error['error'] = Response::$statusTexts[$statusCode]; if (env('APP_DEBUG')) { $error['message'] = $e->getMessage(); $error['file'] = $e->getFile() . ':' . $e->getLine(); $error['trace'] = explode("\n", $e->getTraceAsString()); } return response()->json($error, $statusCode, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); } Rob Allen ~ @akrabat
  71. Override ExceptionsHandler::render() public function render($request, Exception $e) { $statusCode =

    $this->getStatusCodeFromException($e); $error['error'] = Response::$statusTexts[$statusCode]; if (env('APP_DEBUG')) { $error['message'] = $e->getMessage(); $error['file'] = $e->getFile() . ':' . $e->getLine(); $error['trace'] = explode("\n", $e->getTraceAsString()); } return response()->json($error, $statusCode, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); } Rob Allen ~ @akrabat
  72. Error output (live) $ curl -i http://localhost:8888/authors/999 HTTP/1.0 404 Not

    Found Content-Type: application/json { "error": "Not Found" } Rob Allen ~ @akrabat
  73. Error output (debug) $ curl -i http://localhost:8888/authors/999 HTTP/1.0 404 Not

    Found Content-Type: application/json { "error": "Not Found", "message": "No query results for model [App\\Author] 999", "file": "vendor/illuminate/database/Eloquent/Builder.php:33 "trace": [ "#0 vendor/illuminate/database/Eloquent/Model.php(1509) "#1 vendor/illuminate/database/Eloquent/Model.php(1521) "#2 app/Http/Controllers/AuthorController.php(30): Illu … Rob Allen ~ @akrabat
  74. To sum up Rob Allen ~ @akrabat

  75. Resources • https://github.com/akrabat/lumen-bookshelf-api • https://lumen.laravel.com/docs/ • https://fractal.thephpleague.com Books: • Build

    APIs You Won't Hate by Phil Sturgeon • RESTful Web APIs by L. Richardson, M. Amundsen & S. Ruby Rob Allen ~ @akrabat
  76. Thank you! Rob Allen ~ @akrabat