Improve your API with OpenAPI Rob Allen PHPSW, November 2021

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 a contract Rob Allen ~ @akrabat

Anatomy of the specification Rob Allen ~ @akrabat

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

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

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

openapi.yaml 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 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

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

openapi.yaml 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 • Editor with plugins: vim, VS Code, etc • GUI: Stoplight, OpenAPI-GUI, Swagger Editor Rob Allen ~ @akrabat

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

Docs Rob Allen ~ @akrabat

Docs Rob Allen ~ @akrabat

Docs Rob Allen ~ @akrabat

Developers Rob Allen ~ @akrabat

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

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 OpenAPI 3.0: league/openapi-psr7-validator OpenAPI 3.1: opis/json-schema Rob Allen ~ @akrabat

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

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

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

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

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

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

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

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

To sum up Rob Allen ~ @akrabat

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

Thank you! https://joind.in/talk/08577 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