Powering Your API Development with
OpenAPI
Sunshine PHP 2020
Daniel Abernathy
About Me
Austin, TX
What's OpenAPI?
"A broadly adopted industry standard for describing modern APIs."
Blueprint
A guide for developers on how to build or consume an API.
Contract
An agreement between all stakeholders on how an API's requests and responses should be structured.
Practical summary:
An OpenAPI document is a machine-readable description of an API's endpoints and the structure of the data in its requests and responses.
2010
- Swagger specification created
2015
- Swagger purchased by SmartBear
- Specification donated to Linux Foundation and rebranded as OpenAPI (v2)
2017
- OpenAPI version 3 released
History
Comparisons
vs. API modeling tools like Postman
OpenAPI
- Models the structure and schema of requests & responses (but also allows examples!)
- Tools are built around OpenAPI specification
Postman
- Models examples of requests
- Tools are bundled with Postman
Comparisons
vs. API format specifications like JSON:API
OpenAPI
- Specification for a document (an OpenAPI file)
- Describes your API in whatever format you choose
- Can model an API that conforms to a specification like JSON:API
- Is concerned with the details of your API endpoints and values
JSON:API
- Specification for your API itself
- Describes a format that your API must conform to
- Isn't concerned with the particular endpoints or values in your API, only the structure
Comparisons
vs. other API description specifications
API Blueprint
RAML
Benefits of starting with OpenAPI
- Provides a contract that all teams can agree on up-front
- Front-end developers can start building against the description sooner
- Back-end developers can write tests to ensure the implementation meets the description
Benefit of having an OpenAPI file
Documentation
Mock Server
Testing
Validation
(... and more)
OpenAPI Resources
OpenAPI Basics
- Auto-generated from code annotations
- Write JSON or YAML by hand
- (side-by-side with rendered view)
- Using IDE tools
- GUI editor (Stoplight)
How to write an OpenAPI file
OpenAPI Basics
{
"openapi": "3.0.0",
"info": {
"title": "Raffle API",
"version": "",
"description": "Raffle Application API"
},
"servers": [],
"paths": {},
"components": {}
}
Paths
"/widgets": {
"get": {
"description": "Get all widgets"
"responses": {
"200": {
"description": "OK"
}
}
}
}
Paths describe your individual endpoints
They primarily contain "operations", which correspond to HTTP verbs
Paths
"/widgets/{widgetId}": {
"parameters": [
{
"schema": {
"type": "integer"
},
"name": "widgetId",
"in": "path",
"required": true
}
]
}
Paths can also contain parameters
Operations
"patch": {
"description": "Update a widget",
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
The most important parts of an operation are the request body and the responses.
Schema
Data in OpenAPI describes schema with JSON Schema.... ish
OpenAPI uses an "extended subset" of an old draft of JSON Schema.
ಠ_ಠ
Schema
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
}
}
}
components & $ref
The root level components property lets you store data that you can reuse throughout document
{
"components": {
"schema": {},
"responses": {},
"parameters": {},
"requestBodies": {}
}
}
components & $ref
The $ref keyword lets you point to an item in the components section from elsewhere in the document.
{
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Widget"
}
}
}
Building a Raffle application with OpenAPI
User
Contest
Contestant
Prize
GET /contests
POST /contests
GET /contests/{contestId}
PATCH /contests/{contestId}
DELETE /contests/{contestId}
GET /contests/{contestId}/prizes
POST /contests/{contestId}/prizes
GET /contests/{contestId}/contestants
POST /contests/{contestId}/contestants
PATCH /contestants/{contestantId}
DELETE /contestants/{contestantId}
PATCH /prizes/{prizeId}
DELETE /prizes/{prizeId}
Plan your endpoints
Start writing your OpenAPI doc
Time for a live demo. 🤠
-
GET /contests - VS Code
-
POST /contests - Stoplight
Testing with your OpenAPI document
public function testCanCreateContest()
{
$response = $this->actingAs(\App\User::first())
->postJson('/contests', ['name' => 'Test Contest']);
// Additional assertions
[$success, $message] = $this->validateWithOpenApi(
'/contests',
'POST',
$response
);
$this->assertTrue($success, $message);
}
Testing with your OpenAPI document
trait ValidatesWithOpenApi
{
protected $validator;
protected function initValidator()
{
$spec_path = base_path('public/raffle-api-spec/reference/raffle-api.json');
$this->validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)
->fromJsonFile($spec_path)
->getResponseValidator();
}
Testing with your OpenAPI document
protected function validateWithOpenApi($path, $method, TestResponse $response)
{
$this->initValidator();
$response = $this->convertResponse($response);
$operation = new OperationAddress($path, strtolower($method));
try {
$this->validator->validate($operation, $response);
$success = true;
$message = '';
} catch (\Exception $e) {
$success = false;
$message = $e->getMessage();
// Get additional validation messages from specific
// validation exceptions
}
return [$success, $message];
}
Testing with your OpenAPI document
Validating with your OpenAPI document
Test that requests result in validation error
public function testContestNameMustBeString()
{
$body = [
'name' => 123,
];
$response = $this->actingAs(\App\User::first())
->postJson('/contests', $body)
->assertStatus(400);
}
Validating with your OpenAPI document
Setting up validation middleware in Laravel
<?php
// app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'openapi-validation' => \League\OpenAPIValidation\PSR15\ValidationMiddleware::class,
];
// Routes file
Route::post('/contests', 'ContestController@create')
->name('contest.create')
->middleware('openapi-validation');
Need PSR-15 adapter for Laravel:
Validating with your OpenAPI document
Handle validation exception
<?php
// app/Exceptions/Handler.php
public function render($request, Exception $exception)
{
if ($exception instanceof \League\OpenAPIValidation\PSR7\Exception\ValidationFailed) {
$message = $exception->getMessage();
// Additional logic to get more specific error message...
app()->abort(400, $message);
}
return parent::render($request, $exception);
}
Validating with your OpenAPI document
Open API Talk
By Daniel Abernathy
Open API Talk
- 1,195