Slide 1

Slide 1 text

Writing APIs in Lumen Rob Allen June 2018

Slide 2

Slide 2 text

I write APIs Rob Allen ~ @akrabat

Slide 3

Slide 3 text

Let's start with Lumen Rob Allen ~ @akrabat

Slide 4

Slide 4 text

Lumen • Microframework • Built with Laravel components • Fast! Rob Allen ~ @akrabat

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Disabled features bootstrap/app.php: // $app->withFacades(); // $app->withEloquent(); Rob Allen ~ @akrabat

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

/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

Slide 16

Slide 16 text

What is an API? Rob Allen ~ @akrabat

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

What is a web API? • Programmatic interface • Endpoints • Request-response message system • JSON or XML • Stateless Rob Allen ~ @akrabat

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Constraints: Client/Server • Clients are not concerned with storage (portable) • Servers are not concerned with UI or user state (scalable) Rob Allen ~ @akrabat

Slide 22

Slide 22 text

Constraints: Stateless • No client context stored between requests. (no sessions!) Rob Allen ~ @akrabat

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Constraints: Layered system • Client should not care whether it is connected directly to the server, or to an intermediary proxy. Rob Allen ~ @akrabat

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Routing in Laravel routes/web.php: $router->get('/authors/{id:\d+}', [ 'as' => 'author.list', 'uses' => 'AuthorController@show' ]); Rob Allen ~ @akrabat

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Hypermedia • Media type used for a representation • The link relations between representations and/or states • Important for discoverability Rob Allen ~ @akrabat

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

AuthorTransformer class AuthorTransformer extends Fractal\TransformerAbstract { public function transform(Author $author) { return [ 'id' => (int) $author->id, 'name' => $author->name, ]; } } Rob Allen ~ @akrabat

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Output $ curl http://localhost:8888/authors/4 { "data": [ { "type": "authors", "id": "1", "attributes": { "name": "Suzanne Collins", }, "links": { "self": "/authors/1" } }, Rob Allen ~ @akrabat

Slide 65

Slide 65 text

When things go wrong Rob Allen ~ @akrabat

Slide 66

Slide 66 text

Default error output $ curl http://localhost:8888/authors/999 /* 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

Slide 67

Slide 67 text

Great error handling • Error representations are first class citizens • Provides application error code & human readable message • Pretty prints for the humans! Rob Allen ~ @akrabat

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

To sum up Rob Allen ~ @akrabat

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Thank you! Rob Allen ~ @akrabat