Improve your API with OpenAPI

A presentation at PHPSW, November 2021 in November 2021 in Bristol, UK by Rob Allen

Slide 1

Slide 1

Improve your API with OpenAPI Rob Allen PHPSW, November 2021

Slide 2

Slide 2

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service https://spec.openapis.org/oas/latest.html Rob Allen ~ @akrabat

Slide 3

Slide 3

It’s about documentation Rob Allen ~ @akrabat

Slide 4

Slide 4

It’s about design-first Rob Allen ~ @akrabat

Slide 5

Slide 5

It’s about communicating changes Rob Allen ~ @akrabat

Slide 6

Slide 6

It’s about development workflows Rob Allen ~ @akrabat

Slide 7

Slide 7

It’s about a contract Rob Allen ~ @akrabat

Slide 8

Slide 8

Anatomy of the specification Rob Allen ~ @akrabat

Slide 9

Slide 9

openapi.yaml openapi: “3.1.0” # or “3.0.3” info: # … servers: # … paths: # … webhooks: # … components: # … security: # … tags: # … externalDocs: # … Rob Allen ~ @akrabat

Slide 10

Slide 10

openapi.yaml info: title: Rock-Paper-Scissors version: “1.0.0” description: > An implementation of Rock-Paper-Scissors. contact: name: “Rob Allen” servers: - url: https://rock-paper-scissors.example.com description: “RPS production API” Rob Allen ~ @akrabat

Slide 11

Slide 11

openapi.yaml paths: post: summary: Create a new game description: > Create a new game of Rock-Paper-Scissors. requestBody: # … responses: # … Rob Allen ~ @akrabat

Slide 12

Slide 12

openapi.yaml requestBody: description: Game to add required: true content: application/json: schema: $ref: ‘#/components/schemas/NewGameRequest’ Rob Allen ~ @akrabat

Slide 13

Slide 13

Reuse of objects $ref allows us to define one & use in many places components: schemas: GameId: type: string examples: - “2BC08389-885A-4322-80D0-EF0DE2D7CD37” format: “uuid” Player: type: string example: “Rob” Rob Allen ~ @akrabat

Slide 14

Slide 14

Build on other components schemas: NewGameRequest: properties: player1: $ref: ‘#/components/schemas/Player’ player2: $ref: ‘#/components/schemas/Player’ required: - player1 - player2 examples: - ‘{“player1”:”Ian”, “player2”:”Dave”}’ Rob Allen ~ @akrabat

Slide 15

Slide 15

openapi.yaml responses: “201”: $ref: ‘#/components/responses/NewGameResponse’ “400”: $ref: ‘#/components/responses/NewGameError’ “500”: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat

Slide 16

Slide 16

Writing your spec Rob Allen ~ @akrabat

Slide 17

Slide 17

Editing • Editor with plugins: vim, VS Code, etc • GUI: Stoplight, OpenAPI-GUI, Swagger Editor Rob Allen ~ @akrabat

Slide 18

Slide 18

Linting CLI tools: Spectral, openapi-spec-validator, etc. $ spectral lint rps-openapi.yaml …/slim4-rps-api/doc/rps-openapi.yaml 3:6 warning info-contact Info object must have “contact” object. info ✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) Rob Allen ~ @akrabat

Slide 19

Slide 19

Docs Rob Allen ~ @akrabat

Slide 20

Slide 20

Docs Rob Allen ~ @akrabat

Slide 21

Slide 21

Docs Rob Allen ~ @akrabat

Slide 22

Slide 22

Developers Rob Allen ~ @akrabat

Slide 23

Slide 23

Mock server $ prism mock rps-openapi.yaml $ curl http://127.0.0.1:4010/games -d ‘{}’ {“message”:”Must provide both player1 and player2”} Rob Allen ~ @akrabat

Slide 24

Slide 24

Validation The schema section can be used to validate the request and response • Validate early and return a 422 • Validate that we return what we say we will • Put it in CI to prevent regressions Rob Allen ~ @akrabat

Slide 25

Slide 25

But I already have validation! Your code: • isn’t good enough! • isn’t reusable! • doesn’t match the docs! Rob Allen ~ @akrabat

Slide 26

Slide 26

But I already have validation! Your code: • isn’t good enough! • isn’t reusable! • doesn’t match the docs! However… Business logic validation still needed! Rob Allen ~ @akrabat

Slide 27

Slide 27

Validation in PHP OpenAPI 3.0: league/openapi-psr7-validator OpenAPI 3.1: opis/json-schema Rob Allen ~ @akrabat

Slide 28

Slide 28

Validation middleware public function process($request, $handler) { // Determine OAS schema for this route into $schema // Validate request $builder = (new ValidatorBuilder())->fromSchema($schema); $validator = $builder->getServerRequestValidator(); try { $match = $validator->validate($request); } catch (ValidationFailed $e) { throw new RequestValidationFailed($e); } Rob Allen ~ @akrabat

Slide 29

Slide 29

Validation middleware public function process($request, handler) { // Determine OAS schema for this route into $schema // Validate request $builder = (new ValidatorBuilder())->fromSchema($schema); $validator = $builder->getServerRequestValidator(); try { $match = $validator->validate($request); } catch (ValidationFailed $e) { throw new RequestValidationFailed($e); } Rob Allen ~ @akrabat

Slide 30

Slide 30

Validation middleware public function process($request, handler) { // Determine OAS schema for this route into $schema // Validate request $builder = (new ValidatorBuilder())->fromSchema($schema); $validator = $builder->getServerRequestValidator(); try { $match = $validator->validate($request); } catch (ValidationFailed $e) { throw new RequestValidationFailed($e); } Rob Allen ~ @akrabat

Slide 31

Slide 31

Validation middleware (cont) // Process request $response = $handler->handle($request); // Validate response try { $builder->getResponseValidator() ->validate($match, $response); } catch (ValidationFailed $e) { throw new ResponseValidationFailed($e); } return $response; } Rob Allen ~ @akrabat

Slide 32

Slide 32

Validation middleware (cont) // Process request $response = $handler->handle($request); // Validate response try { $builder->getResponseValidator() ->validate($match, $response); } catch (ValidationFailed $e) { throw new ResponseValidationFailed($e); } return $response; } Rob Allen ~ @akrabat

Slide 33

Slide 33

Validation middleware (cont) // Process request $response = $handler->handle($request); // Validate response try { $builder->getResponseValidator() ->validate($match, $response); } catch (ValidationFailed $e) { throw new ResponseValidationFailed($e); } return $response; } Rob Allen ~ @akrabat

Slide 34

Slide 34

Validation middleware (cont) // Process request $response = $handler->handle($request); // Validate response try { $builder->getResponseValidator() ->validate($match, $response); } catch (ValidationFailed $e) { throw new ResponseValidationFailed($e); } return $response; } Rob Allen ~ @akrabat

Slide 35

Slide 35

Validation Middleware in Laravel // composer install softonic/laravel-psr15-bridge // app/Http/Kernel.php use \League\OpenAPIValidation\PSR15\ValidationMiddleware; protected $routeMiddleware = [ // … ‘openapi-validation’ => ValidationMiddleware::class, ]; // Routes file Route::post(‘/game’, ‘GameController@create’) ->middleware(‘openapi-validation’); Rob Allen ~ @akrabat

Slide 36

Slide 36

To sum up Rob Allen ~ @akrabat

Slide 37

Slide 37

Resources • https://www.openapis.org • https://openapi.tools • https://github.com/akrabat/slim4-rps-api • https://github.com/thephpleague/openapi-psr7-validator • https://github.com/primitivesocial/openapi-initializer Rob Allen ~ @akrabat

Slide 38

Slide 38

Thank you! https://joind.in/talk/08577 Rob Allen ~ @akrabat

Slide 39

Slide 39

Photo credits - Scaffolding: https://www.flickr.com/photos/pagedooley/49683539647 - Writing: https://www.flickr.com/photos/throughkikslens/14516757158 - Books: https://www.flickr.com/photos/eternaletulf/41166888495 - Computer code: https://www.flickr.com/photos/n3wjack/3856456237 - Rocket launch: https://www.flickr.com/photos/gsfc/16495356966 - Stars: https://www.flickr.com/photos/gsfc/19125041621 Rob Allen ~ @akrabat