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

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.

Rob Allen

June 11, 2018
Tweet

More Decks by Rob Allen

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. /ping: An API's "Hello World" routes/web.php: $router->get('/ping', function () {

    return response()->json(['ack' => time()]); }); Rob Allen ~ @akrabat
  10. /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
  11. 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
  12. What is a web API? • Programmatic interface • Endpoints

    • Request-response message system • JSON or XML • Stateless Rob Allen ~ @akrabat
  13. 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
  14. 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
  15. Constraints: Client/Server • Clients are not concerned with storage (portable)

    • Servers are not concerned with UI or user state (scalable) Rob Allen ~ @akrabat
  16. 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
  17. Constraints: Layered system • Client should not care whether it

    is connected directly to the server, or to an intermediary proxy. Rob Allen ~ @akrabat
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. Routes have a pattern • Literal string match $router->get('/authors', …);

    • Placeholders are wrapped in { } $router->get('/authors/{id}', …); Rob Allen ~ @akrabat
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. Hypermedia • Media type used for a representation • The

    link relations between representations and/or states • Important for discoverability Rob Allen ~ @akrabat
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. AuthorTransformer class AuthorTransformer extends Fractal\TransformerAbstract { public function transform(Author $author)

    { return [ 'id' => (int) $author->id, 'name' => $author->name, ]; } } Rob Allen ~ @akrabat
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. Output $ curl http://localhost:8888/authors/4 { "data": [ { "type": "authors",

    "id": "1", "attributes": { "name": "Suzanne Collins", }, "links": { "self": "/authors/1" } }, Rob Allen ~ @akrabat
  56. 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
  57. Great error handling • Error representations are first class citizens

    • Provides application error code & human readable message • Pretty prints for the humans! Rob Allen ~ @akrabat
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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