Learning outcomes: By the end of this blog, you should be able to:
- Understand what OpenAPI is
- Define APIs using OpenAPI specification.
- Differentiate OpenAPI and Swagger.
- Understand API-first approach and why it is a good choice.
OpenAPI vs Swagger
Fun fact: OpenAPI is not OpenAI, it doesn’t develop ChatGPT.
There is often confusion regarding the differences between OpenAPI and Swagger. While these terms are sometimes used interchangeably, they actually refer to distinct entities.
OpenAPI Specification
According to https://github.com/OAI/OpenAPI-Specification:
The OpenAPI Specification (OAS) defines a standard and programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. When properly defined via OpenAPI, a consumer can understand and interact with the remote service with minimal implementation logic. Similar to what interface descriptions do for lower-level programming, the OpenAPI Specification removes the guesswork in calling a service.
Use cases for machine-readable API definition documents include, but are not limited to: interactive documentation; code generation for documentation, clients, and servers; and automation of test cases. OpenAPI documents describe APIs services and are represented in YAML or JSON formats. These documents may be produced and served statically or generated dynamically from an application.
OpenAPI Specification provides a standard interface description for HTTP APIs, making it easy for humans and computers to understand and interact with a HTTP server. Tools such as interactive documentation, code generation, and test automation can be built on top of the specification.
The following snippet demonstrates how the APIs can be defined in the OpenAPI specification.
openapi: 3.1.0
info:
title: Stock Exchange
version: 1.0.0
servers:
- url: "https://{env}-api.example.com"
variables:
env:
description: Environment
default: dev
enum: [dev, uat, prod]
paths:
/api/v1/stocks:
post:
tags:
- Stock
summary: Lists a new stock.
operationId: listStock
description: Lists a new Stock to the Exchange.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Stock"
required: true
responses:
201:
description: Successfully listed a new stock.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/BaseSuccessResponse"
- type: object
properties:
data:
$ref: "#/components/schemas/Stock"
400:
description: Listing failed due to client error, such as invalid or duplicated symbol.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/BaseFailureResponse"
403:
description: User or client app does NOT have listing stock permission.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/BaseFailureResponse"
/api/v1/stocks/{symbol}:
# ...
/api/v1/orders:
# ...
/api/v1/orders/{id}:
# ...
Like many specifications, OpenAPI is lengthy and formal. However, in practice, only a small subset of its elements are commonly used. Let’s breakdown the snippet above to understand how commonly used OpenAPI elements match to a typical HTTP request and response.
- Host: Backend applications are usually deployed on different environments such as DEV, UAT, and PROD. OpenAPI offers the ‘servers’ field, which is an array, to support multiple available servers. The server URLs can also be parameterised, which can be useful when the URLs follow a particular pattern.
servers:
- url: 'https://{env}-api.example.com'
variables:
env:
description: Environment
default: dev
enum: [ dev, uat, prod ]
- Path and path parameter: Under the OpenAPI specification, each API path is declared as a child element under the
paths
element. When an API path contains parameters, such as{id}
, it is recommended to define them in theparameters
element right under the path. This helps to avoid defining the parameters repeatedly in each operation (VERB + path).
paths:
/api/v1/stocks:
# [...]
/api/v1/stocks/{symbol}:
parameters:
- name: symbol
in: path
required: true
schema:
type: string
- Query parameter: Query parameters are similar to path parameters, but they come with
in: query
instead ofin: path
. If the query parameter is common for all operations under the same path, it should be declared in theparameters
section under the path. However, if the query parameter is specific to a certain operation, it can be defined under that operation.
paths:
/api/v1/stocks:
get:
parameters:
- name: sort_by
in: query
description: "Sort results by"
required: false
schema:
type: string
- Cookie and Header: Other than
path
andquery
,parameters
also supportscookie
andheader
. Using them is very much the same with the formers. Commonly used parameter can be extracted to a global param and referred where it is needed.
paths:
/api/v1/stocks/{symbol}:
parameters:
- $ref: '#/components/parameters/headerXUserId'
components:
parameters:
headerXUserId:
name: X-User-Id
in: header
description: "User ID header"
schema:
type: string
- Verb: HTTP verbs, such as GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD. In OpenAPI spec, Verbs are a child element of the
path
element and must be written in lowercase. Verb and Path form an operation. Each verb has its own set of required child elements. If you are using an OpenAPI-aware editor, it will likely suggest any missing child elements. To make the API definition more descriptive, it is recommended to usesummary
description
andoperationId
, although they are optional.
-summary
: A short summary of what the operation does.
-description
: A verbose explanation of the operation behavior.
-operationId
: Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions.
paths:
/api/v1/stocks:
post:
summary: Lists a new stock.
operationId: listStock
description: Lists a new Stock to the Exchange.
parameters:
- ...
requestBody:
[...]
responses:
[...]
- Request Body: Defines the request body schema. Content type can be any among variety of supported media types, most commonly used are
application/json
andapplication/xml
. Schema type can beobject
array
or primitive types such asstring
boolean
integer
andnumber
. Large and repeatedly used types should be extracted tocomponents.schemas
and referred in APIs with$ref
similar toparameters
paths:
post:
tags:
- Stock
summary: Lists a new stock.
operationId: listStock
description: Lists a new Stock to the Exchange.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Stock"
required: true
responses:
[...]
components:
schemas:
Symbol:
type: string
description: Symbol of the Stock, it must be unique on the exchange. Symbol contains 4 upper cases alphabetical characters.
example: "AAPL"
Stock:
type: object
properties:
symbol:
$ref: "#/components/schemas/Symbol"
company:
type: string
example: "Apple Inc."
- Status Code and Response Body: Status codes refer to HTTP Status codes. They are defined in
responses
under each operation (VERB+Path). We should enumerate failure status codes along with success status codes. E.g. we all know that 400 is Bad Request, but what might be the cause, any hints? My experience tells me that the more detailed we write, the less they (e.g. clients, QA) ask. Each status code comes withcontent
where we can define the response body, in the same way we declare Request Body.
paths:
/api/v1/stocks:
post:
tags:
- Stock
summary: Lists a new stock.
operationId: listStock
description: Lists a new Stock to the Exchange.
requestBody:
# [...]
responses:
201:
description: Successfully listed a new stock.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/BaseSuccessResponse"
- type: object
properties:
data:
$ref: "#/components/schemas/Stock"
400:
description: Listing failed due to client error, such as invalid or duplicated symbol.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/BaseFailureResponse"
403:
description: User or client app does NOT have listing stock permission.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/BaseFailureResponse"
components:
schemas:
BaseSuccessResponse:
type: object
properties:
message:
type: string
example: "Successfully"
BaseFailureResponse:
type: object
properties:
message:
type: string
example: "Failed"
errorCode:
type: string
example: "ERR1001"
Symbol:
type: string
description: Symbol of the Stock, it must be unique on the exchange. Symbol contains 4 upper cases alphabetical characters.
example: "AAPL"
Stock:
type: object
properties:
symbol:
$ref: "#/components/schemas/Symbol"
company:
type: string
example: "Apple Inc."
Remembering even a small subset of OpenAPI elements can be challenging. Thankfully, we don’t usually have to. Instead, we can define one or two APIs properly and refer to them for other APIs. Many editors also support “code completion” and validation for OpenAPI spec too, e.g. SwaggerEditor. When it is necessary, we can always refer to the Specification at https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md.
Swagger
Simply put, OpenAPI defines the specification, while Swagger is a set of tools built around the spec, e.g Swagger Editor, Swagger UI and SwaggerHub.
Swagger and Swagger Specification were twins. The confusion began in 2016 when Swagger Specification was renamed to OpenAPI Specification while Swagger continued to refer to the set of tools provided by SmartBear.
Swagger Editor
Swagger Editor is a web-based tool that makes writing API specifications in OpenAPI format efficient with features such as code completion and validation. Additionally, Swagger Editor comes with Swagger UI to automatically generates an interactive API Documentation as we type. While Swagger Editor is ready to use at https://editor.swagger.io/, it can be deployed to any host, including localhost. For more information, visit https://github.com/swagger-api/swagger-editor.
NOTE: Surprisingly, Swagger Editor didn’t support OpenAPI 3.1.0 at the time of writting in February 2023.
Swagger UI
Swagger UI is a tool used to visualize OpenAPI specifications. It is embedded in Swagger Editor and can also be used as a standalone component. Some teams use it on Docker or even include it in their backend application. GitLab is also able to recognise OpenAPI files and render an API document page using Swagger UI.
Swagger Codegen
While it’s technically feasible to generate code from a well-defined API specification, it’s not a commonly practiced approach among teams. This could be because stub server and client codes can be written quickly without the use of code generation tools. However, if you find Swagger Codegen useful, then there’s no harm in using it.
Other tools
Swagger has been around since the first day, and is widely popular due to its user-friendliness. Nevertheless, there are other tools that depend on the specifications, such as Postman and Redoc.
Swagger Plugin for JetBrains IDEs
If you are JetBrains IDEs’ lover, you might find Swagger plugin useful. It brings Swagger Editor and Swagger UI into the same IDEs that you use to develop applications.
Postman
It’s not a surprise that Postman, one or the most famous HTTP client tools, supports OpenAPI specification. We can define API spec on Postman then create a Collection from such definition.
ReDoc
ReDoc is another API docs tool. It is as great as Swagger UI. If you find ReDoc presents API docs more nicely, then you can go with it.
Redocly offers a VSCode extension that not only enhances our API spec writing experience, but also elevates the quality of our OpenAPI definitions. With the extension, we can benefit from strict syntax validation, ensuring that our API specification adheres to best practices and is of high quality.
API First approach
Consider this scenario: You are in a kick-off meeting for a new project. The project requires modifications to three different backend services maintained by separate teams, as well as changes to the frontend. Each team requires between two to three weeks to implement the changes. Given these constraints, how can we ensure that the project is fully functional and well tested within four weeks? This is where the API First approach becomes invaluable.
An API-first approach treats APIs as top priority in a development project. It involves developing consistent and reusable APIs using an API description language to establish a contract for how the API behaves. This requires spending more time on API design and collaborating with stakeholders for feedback before any code is written.
API-first approach brings many benefits to developing software. Here are just a few:
- Faster time-to-market: Adopting an API-first approach, development teams can work in parallel and achieve faster delivery of high-quality software. The approach ensures that the API aligns with the needs of both the application and the end-users, resulting in a faster time-to-market.
- More reliable APIs: Developing well-designed, well-documented, and consistent APIs using an API-first approach leads to more reliable APIs, which can help avoid unexpected changes. This saves developers from spending sleepless nights investigating issues caused by unanticipated API changes.
- Reusable APIs: APIs developed in an API-first approach are likely to be reusable by different consuming applications because they are designed with a focus on consistency, modularity, and reusability. New coming developers can easily understand how to interact with the API by exploring its well document.
A comprehensive guide to planning and implementing API-first can be found at: https://swagger.io/resources/articles/adopting-an-api-first-approach/. The short-and-sweet answer is illustrated in the diagram below:
The API provider should design the APIs first and send it to the consumer to review. The reviewing process should be continuous. We can also setup a code reviewing rule to enforce that every change to API spec must be approved by at least one developer from the consuming team. Upon agreed, the provider should provide a mock server in order to enable early integration for the consumer. There are bunch of tools for that purpose. It should be trivial to implement a mock server too. After that, the provider can start working on the actual implementation. When an API is fully implemented, we should replace the mock with it. If both sides comply with the spec, the end to end integration should ideally be seamless. The process repeats until the last API is fully integrated.
Stock Exchange Application
Fully defined APIs specification for the Stock Exchange application is committed to https://gitlab.com/briannqc/backend-101/-/blob/main/openapi.yaml. This repository will also be used in upcoming blogs.
Exercise 1. Defining OpenAPI spec for any of your APIs, e.g. the one you designed in the last blog: https://medium.com/@briannqc/backend-101-designing-effective-rest-apis-362ca0c91b80
That’s long enough, see you in the next blog post!