The How and Why of OpenAPI Rob Allen Web Summer Camp, July 2024
A presentation at Web Summer Camp 2024 in July 2024 in Opatija, Croatia by Rob Allen
 
                The How and Why of OpenAPI Rob Allen Web Summer Camp, July 2024
 
                APIs Power the Internet Rob Allen ~ @akrabat
 
                APIs Power the Internet API Descriptions Power APIs Rob Allen ~ @akrabat
 
                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
 
                It’s about documentation Rob Allen ~ @akrabat
 
                It’s about design-first Rob Allen ~ @akrabat
 
                It’s about communicating changes Rob Allen ~ @akrabat
 
                It’s about development workflows Rob Allen ~ @akrabat
 
                It’s about standardisation Rob Allen ~ @akrabat
 
                It’s about a contract Rob Allen ~ @akrabat
 
                “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
 
                Anatomy of the specification Rob Allen ~ @akrabat
 
                openapi.yaml openapi: “3.1.0” info: # … servers: # … paths: # … webhooks: # … components: # … security: # … tags: # … externalDocs: # … Rob Allen ~ @akrabat
 
                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
 
                Endpoints paths: ‘/games’: get: # … post: # … ‘/games/{game_id}/moves’: post: # … ‘/games/{game_id}/judgement’: get: # … Rob Allen ~ @akrabat
 
                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
 
                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
 
                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
 
                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
 
                RequestBody requestBody: description: Game to add required: true content: application/json: schema: $ref: ‘#/components/schemas/NewGameRequest’ Rob Allen ~ @akrabat
 
                RequestBody requestBody: description: Game to add required: true content: application/json: schema: $ref: ‘#/components/schemas/NewGameRequest’ Rob Allen ~ @akrabat
 
                RequestBody requestBody: description: Game to add required: true content: application/json: schema: $ref: ‘#/components/schemas/NewGameRequest’ Rob Allen ~ @akrabat
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                RequestBody requestBody: description: Game to add required: true content: application/json: schema: $ref: ‘#/components/schemas/NewGameRequest’ Rob Allen ~ @akrabat
 
                Responses responses: ‘201’: $ref: ‘#/components/responses/NewGameResponse’ ‘400’: $ref: ‘#/components/responses/NewGameError’ ‘500’: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat
 
                Responses responses: ‘201’: $ref: ‘#/components/responses/NewGameResponse’ ‘400’: $ref: ‘#/components/responses/NewGameError’ ‘500’: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat
 
                Responses responses: ‘201’: $ref: ‘#/components/responses/NewGameResponse’ ‘400’: $ref: ‘#/components/responses/NewGameError’ ‘500’: $ref: ‘#/components/responses/InternalServerError’ Rob Allen ~ @akrabat
 
                Writing your spec Rob Allen ~ @akrabat
 
                Editing It’s just text! Rob Allen ~ @akrabat
 
                Editing GUI tools: Stoplight, OpenAPI-GUI, Swagger Editor Rob Allen ~ @akrabat
 
                Linting & validation CLI tools: Spectral, openapi-spec-validator, etc. $ spectral lint openapi.yaml No results with a severity of ‘error’ or higher found! Rob Allen ~ @akrabat
 
                Validation error $ spectral lint openapi.yaml …/slim4-rps-api/doc/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
 
                Coding Time! Write an OpenAPI spec Rob Allen ~ @akrabat
 
                Docs Rob Allen ~ @akrabat
 
                Docs Rob Allen ~ @akrabat
 
                Docs Rob Allen ~ @akrabat
 
                Docs Rob Allen ~ @akrabat
 
                Docs Rob Allen ~ @akrabat
 
                Docs Rob Allen ~ @akrabat
 
                Demo Time! Generating docs Rob Allen ~ @akrabat
 
                Developers Rob Allen ~ @akrabat
 
                Mock server $ prism mock openapi.yaml Rob Allen ~ @akrabat
 
                Make API calls $ curl http://127.0.0.1:4010/games -d ‘{}’ Rob Allen ~ @akrabat
 
                Make API calls $ curl http://127.0.0.1:4010/games -d ‘{}’ {“message”:”Must provide both player1 and player2”} Rob Allen ~ @akrabat
 
                Make API calls $ curl http://127.0.0.1:4010/games -d ‘{}’ {“message”:”Must provide both player1 and player2”} Rob Allen ~ @akrabat
 
                Demo Time! Using a mock server Rob Allen ~ @akrabat
 
                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
 
                But I already have validation! Your code: • isn’t good enough! • isn’t reusable! • doesn’t match the docs! Rob Allen ~ @akrabat
 
                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
 
                Validation in PHP • league/openapi-psr7-validator • opis/json-schema Rob Allen ~ @akrabat
 
                Validation middleware Rob Allen ~ @akrabat
 
                Test Request Rob Allen ~ @akrabat
 
                Request is invalid Rob Allen ~ @akrabat
 
                Request is invalid Rob Allen ~ @akrabat
 
                Test Request Rob Allen ~ @akrabat
 
                Request is valid Rob Allen ~ @akrabat
 
                Test Response Rob Allen ~ @akrabat
 
                Response is invalid Rob Allen ~ @akrabat
 
                Response is invalid Rob Allen ~ @akrabat
 
                Successful validation Rob Allen ~ @akrabat
 
                Successful validation Rob Allen ~ @akrabat
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                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
 
                Validation middleware public function process($request, $handler) { … // process $response = $handler->handle($request); Rob Allen ~ @akrabat
 
                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
 
                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
 
                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
 
                Coding Time! Validating a PHP API Rob Allen ~ @akrabat
 
                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/openapi.yaml Rob Allen ~ @akrabat
 
                Compliance Testing Rob Allen ~ @akrabat
 
                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
 
                To sum up Rob Allen ~ @akrabat
 
                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
 
                Rob Allen ~ @akrabat
 
                Thank you! Rob Allen ~ @akrabat
 
                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
