Improve your API with OpenAPI

A presentation at PHP UK in February 2022 in London, UK by Rob Allen

Slide 1

Slide 1

Improve your API with OpenAPI Rob Allen PHPUK, February 2022

Slide 2

Slide 2

APIs Power the Internet Rob Allen ~ @akrabat

Slide 3

Slide 3

APIs Power the Internet API Descriptions Power APIs Rob Allen ~ @akrabat

Slide 4

Slide 4

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 5

Slide 5

It’s about documentation Rob Allen ~ @akrabat

Slide 6

Slide 6

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

Slide 7

Slide 7

It’s about communicating changes Rob Allen ~ @akrabat

Slide 8

Slide 8

It’s about development workflows Rob Allen ~ @akrabat

Slide 9

Slide 9

It’s about standardisation Rob Allen ~ @akrabat

Slide 10

Slide 10

It’s about a contract Rob Allen ~ @akrabat

Slide 11

Slide 11

“Using a consistent API description will help increase adoption of APIs across government by reducing time spent in understanding different APIs. gov.uk Rob Allen ~ @akrabat

Slide 12

Slide 12

Anatomy of the specification Rob Allen ~ @akrabat

Slide 13

Slide 13

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

Slide 14

Slide 14

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

Slide 15

Slide 15

Endpoints paths: /games: get: # … post: # … ‘/games/{game_id}/moves’: post: # … ‘/games/{game_id}/judgement’: get: # … Rob Allen ~ @akrabat

Slide 16

Slide 16

Endpoints paths: /games: post: operationId: createGame summary: Create a new game description: Create a new game of Rock-Paper-Scissors. requestBody: # … responses: # … Rob Allen ~ @akrabat

Slide 17

Slide 17

Endpoints paths: /games: post: operationId: createGame summary: Create a new game description: Create a new game of Rock-Paper-Scissors. requestBody: # … responses: # … Rob Allen ~ @akrabat

Slide 18

Slide 18

Endpoints paths: /games: post: operationId: createGame summary: Create a new game description: Create a new game of Rock-Paper-Scissors. requestBody: # … responses: # … Rob Allen ~ @akrabat

Slide 19

Slide 19

Endpoints paths: /games: post: operationId: createGame summary: Create a new game description: Create a new game of Rock-Paper-Scissors. requestBody: # … responses: # … Rob Allen ~ @akrabat

Slide 20

Slide 20

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

Slide 21

Slide 21

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

Slide 22

Slide 22

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

Slide 23

Slide 23

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

Slide 24

Slide 24

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

Slide 25

Slide 25

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

Slide 26

Slide 26

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

Slide 27

Slide 27

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

Slide 28

Slide 28

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

Slide 29

Slide 29

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

Slide 30

Slide 30

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

Slide 31

Slide 31

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

Slide 32

Slide 32

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

Slide 33

Slide 33

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

Slide 34

Slide 34

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

Slide 35

Slide 35

Responses responses: ‘201’: $ref: ‘#/components/responses/NewGameResponse’ ‘400’: $ref: ‘#/components/responses/NewGameError’ ‘500’: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat

Slide 36

Slide 36

Responses responses: ‘201’: $ref: ‘#/components/responses/NewGameResponse’ ‘400’: $ref: ‘#/components/responses/NewGameError’ ‘500’: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat

Slide 37

Slide 37

Responses responses: ‘201’: $ref: ‘#/components/responses/NewGameResponse’ ‘400’: $ref: ‘#/components/responses/NewGameError’ ‘500’: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat

Slide 38

Slide 38

Writing your spec Rob Allen ~ @akrabat

Slide 39

Slide 39

Editing It’s just text! Rob Allen ~ @akrabat

Slide 40

Slide 40

Editing GUI tools: Stoplight, OpenAPI-GUI, Swagger Editor Rob Allen ~ @akrabat

Slide 41

Slide 41

Linting & validation CLI tools: Spectral, openapi-spec-validator, etc. $ spectral lint rps-openapi.yaml No results with a severity of ‘error’ or higher found! Rob Allen ~ @akrabat

Slide 42

Slide 42

Validation error $ 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 43

Slide 43

Docs Rob Allen ~ @akrabat

Slide 44

Slide 44

Docs Rob Allen ~ @akrabat

Slide 45

Slide 45

Docs Rob Allen ~ @akrabat

Slide 46

Slide 46

Docs Rob Allen ~ @akrabat

Slide 47

Slide 47

Docs Rob Allen ~ @akrabat

Slide 48

Slide 48

Docs Rob Allen ~ @akrabat

Slide 49

Slide 49

Developers Rob Allen ~ @akrabat

Slide 50

Slide 50

Mock server $ prism mock rps-openapi.yaml Rob Allen ~ @akrabat

Slide 51

Slide 51

Make API calls $ curl http://127.0.0.1:4010/games -d ‘{}’ Rob Allen ~ @akrabat

Slide 52

Slide 52

Make API calls $ curl http://127.0.0.1:4010/games -d ‘{}’ {“message”:”Must provide both player1 and player2”} Rob Allen ~ @akrabat

Slide 53

Slide 53

Make API calls $ curl http://127.0.0.1:4010/games -d ‘{}’ {“message”:”Must provide both player1 and player2”} Rob Allen ~ @akrabat

Slide 54

Slide 54

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 55

Slide 55

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

Slide 56

Slide 56

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 57

Slide 57

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

Slide 58

Slide 58

Validation middleware Rob Allen ~ @akrabat

Slide 59

Slide 59

Test Request Rob Allen ~ @akrabat

Slide 60

Slide 60

Request is invalid Rob Allen ~ @akrabat

Slide 61

Slide 61

Request is invalid Rob Allen ~ @akrabat

Slide 62

Slide 62

Test Request Rob Allen ~ @akrabat

Slide 63

Slide 63

Request is valid Rob Allen ~ @akrabat

Slide 64

Slide 64

Test Response Rob Allen ~ @akrabat

Slide 65

Slide 65

Response is invalid Rob Allen ~ @akrabat

Slide 66

Slide 66

Response is invalid Rob Allen ~ @akrabat

Slide 67

Slide 67

Successful validation Rob Allen ~ @akrabat

Slide 68

Slide 68

Successful validation Rob Allen ~ @akrabat

Slide 69

Slide 69

Validation middleware class OpenApiValidationMiddleware implements MiddlewareInterface { public function __construct(string $oasFilename, Cache $cache) { $builder = new ValidatorBuilder(); $builder->fromYamlFile($oasFilename); $builder->setCache($cache)->overrideCacheKey(‘openapi’); $this->reqValidator = $builder->getServerRequestValidator(); $this->respValidator = $builder->getResponseValidator(); } public function process($request, $handler) { try { // validate request $match = $this->reqValidator->validate($request); } catch (ValidationFailed $e) { throw new HttpException($request, 422, $e); } // process $response = $handler->handle($request); try { // validate response $this->respValidator->validate($match, $response); return $response; } catch (ValidationFailed $e) { throw new HttpException($request, 500, $e); } } } Rob Allen ~ @akrabat

Slide 70

Slide 70

Validation middleware public function __construct(string $oasFilename, Cache $cache) { $builder = new ValidatorBuilder(); $builder->fromYamlFile($oasFilename); $builder->setCache($cache)->overrideCacheKey(‘openapi’); $this->reqValidator = $builder->getServerRequestValidator(); $this->respValidator = $builder->getResponseValidator(); } Rob Allen ~ @akrabat

Slide 71

Slide 71

Validation middleware public function __construct(string $oasFilename, Cache $cache) { $builder = new ValidatorBuilder(); $builder->fromYamlFile($oasFilename); $builder->setCache($cache)->overrideCacheKey(‘openapi’); $this->reqValidator = $builder->getServerRequestValidator(); $this->respValidator = $builder->getResponseValidator(); } Rob Allen ~ @akrabat

Slide 72

Slide 72

Validation middleware public function __construct(string $oasFilename, Cache $cache) { $builder = new ValidatorBuilder(); $builder->fromYamlFile($oasFilename); $builder->setCache($cache)->overrideCacheKey(‘openapi’); $this->reqValidator = $builder->getServerRequestValidator(); $this->respValidator = $builder->getResponseValidator(); } Rob Allen ~ @akrabat

Slide 73

Slide 73

Validation middleware public function __construct(string $oasFilename, Cache $cache) { $builder = new ValidatorBuilder(); $builder->fromYamlFile($oasFilename); $builder->setCache($cache)->overrideCacheKey(‘openapi’); $this->reqValidator = $builder->getServerRequestValidator(); $this->respValidator = $builder->getResponseValidator(); } Rob Allen ~ @akrabat

Slide 74

Slide 74

Validation middleware public function process($request, $handler) { try { // validate request $match = $this->reqValidator->validate($request); } catch (ValidationFailed $e) { throw new HttpException($request, 422, $e); } Rob Allen ~ @akrabat

Slide 75

Slide 75

Validation middleware public function process($request, $handler) { try { // validate request $match = $this->reqValidator->validate($request); } catch (ValidationFailed $e) { throw new HttpException($request, 422, $e); } Rob Allen ~ @akrabat

Slide 76

Slide 76

Validation middleware public function process($request, $handler) { try { // validate request $match = $this->reqValidator->validate($request); } catch (ValidationFailed $e) { throw new HttpException($request, 422, $e); } Rob Allen ~ @akrabat

Slide 77

Slide 77

Validation middleware public function process($request, $handler) { … // process $response = $handler->handle($request); Rob Allen ~ @akrabat

Slide 78

Slide 78

Validation middleware public function process($request, $handler) { … try { // validate response $this->respValidator->validate($match, $response); return $response; } catch (ValidationFailed $e) { throw new HttpException($request, 500, $e); } } Rob Allen ~ @akrabat

Slide 79

Slide 79

Validation middleware public function process($request, $handler) { … try { // validate response $this->respValidator->validate($match, $response); return $response; } catch (ValidationFailed $e) { throw new HttpException($request, 500, $e); } } Rob Allen ~ @akrabat

Slide 80

Slide 80

Validation middleware public function process($request, $handler) { … try { // validate response $this->respValidator->validate($match, $response); return $response; } catch (ValidationFailed $e) { throw new HttpException($request, 500, $e); } } Rob Allen ~ @akrabat

Slide 81

Slide 81

Compliance Testing Schemathesis reads your OpenAPI spec and tests your API against it pip install schemathesis schemathesis run —stateful=links —checks all \ —base-url=http://localhost:8888 \ doc/rps-openapi.yaml Rob Allen ~ @akrabat

Slide 82

Slide 82

Compliance Testing Rob Allen ~ @akrabat

Slide 83

Slide 83

Other Interesting Tools • • • • Optic: BC Break Detection php-openapi-faker: Create fake data from OpenAPI spec Response2Schema: Generate OpenAPI spec from JSON object Laravel OpenAPI: Generate OpenAPI spec from a Laravel app Many more at https://openapi.tools Rob Allen ~ @akrabat

Slide 84

Slide 84

To sum up Rob Allen ~ @akrabat

Slide 85

Slide 85

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

Slide 86

Slide 86

Rob Allen ~ @akrabat

Slide 87

Slide 87

Thank you! Rob Allen ~ @akrabat

Slide 88

Slide 88

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