Powering Your API Development with

 

OpenAPI

Sunshine PHP 2020

Daniel Abernathy

About Me

Austin, TX

Laravel

Vue.js

Longhorn PHP Organizer

What's OpenAPI?

"A broadly adopted industry standard for describing modern APIs."

openapis.org

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. 🤠

 

  1. GET /contests   -   VS Code

  2. 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:

softonic/laravel-psr15-bridge

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