The Power of OpenAPI: Simplifying API Design and Documentation
Introduction
In the rapidly evolving landscape of software development, creating robust and user-friendly APIs has become essential. One tool that has gained immense popularity for designing, documenting, and testing APIs is OpenAPI.
In this comprehensive guide, we’ll explore why OpenAPI is so important, how to write an OpenAPI document, and the key sections you need to know. Whether you’re a seasoned developer or just getting started, mastering OpenAPI can greatly enhance your API development process.
Why OpenAPI Matters
OpenAPI, formerly known as Swagger, is an open-standard format for describing APIs. It serves as both a machine-readable and human-friendly documentation that enables developers to understand, visualize, and interact with APIs effortlessly. By defining your API using OpenAPI, you unlock a host of benefits:
- Clear Documentation: OpenAPI provides a clear and structured way to document your API, making it easy for both developers and non-developers to understand how to use it.
- Consistency: With a standardized format, your API documentation and implementation remain consistent, reducing confusion and enhancing collaboration among teams.
- Code Generation: OpenAPI enables automatic code generation for client libraries and server stubs in various programming languages, saving time and effort during development.
- Testing and Validation: The OpenAPI specification can be used to validate requests and responses, ensuring that your API adheres to the defined contract.
- Interactive Documentation: Tools like Swagger UI and ReDoc allow you to create interactive documentation that lets users explore and test your API in real-time.
Summary for those short on time
OpenAPI is straightforward to write once you understand the high-level structure.
Below is an OpenAPI document to get you going as a basic example
but read the full post to understand the details.
Also check out the related blog post on Fastly’s dev.to blog:
Better Fastly API clients with OpenAPI Generator
openapi: 3.0.3
info:
title: Your API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/teams/{team_id}/members:
parameters:
- $ref: "#/components/parameters/team_id"
get:
summary: List team members
description: List all members for the specified team.
operationId: list-members
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/list_members_response"
examples:
body:
$ref: "#/components/examples/list_members_response"
components:
parameters:
team_id:
name: team_id
in: path
required: true
style: simple
schema:
$ref: "#/components/schemas/team_id"
schemas:
team_id:
type: string
description: Alphanumeric string identifying a team.
example: AB1C2defGhijKLMNop3qR
list_members_response:
type: array
description: List of members within the specified team.
items:
type: string
examples:
list_members_response:
value:
- "Andrew"
- "Bob"
- "Christine"
The above example OpenAPI document describes an API. Specifically it describes:
- The OpenAPI version supported (i.e.
openapi:
) - Some API Metadata (i.e.
info:
) - The API address (i.e.
server:
) - Supported API endpoints (i.e.
paths:
) - Different ‘components’ referenced by the
paths:
configuration (i.e.components:
)
In practice the last section components:
is where most of the ‘meat’ of the
API configuration happens. You’ll see it contains different sections like
parameters
, schemas
and examples
.
The paths:
section typically doesn’t define behaviours inline but instead
will reference objects defined inside of components:
whenever they need to
describe some behaviour of the endpoint.
Any time you see $ref
that means we’re about to reference an object defined
elsewhere (might be in the same file, under components:
or it could be from a
separate file).
So in the above example we can see the paths:
config references a few
different components:
#/components/parameters/team_id
: this describes the API path’steam_id
input parameter.- This parameter object also references a component (
#/components/schemas/team_id
) for describing the team_id.
- This parameter object also references a component (
#/components/schemas/list_members_response
: this describes the schema for how the response body should look for this API endpoint.#/components/examples/list_members_response
: this demonstrates an example of what the schema looks like in practice.
This is the basic structure of an OpenAPI document. Yes, they can become more complex as the API grows, but at its foundation you will always find this familiar structure.
OK, we’ve got the quick “summary” out of the way, let’s dig a little deeper…
Getting Started with OpenAPI
Before diving into the intricacies of writing an OpenAPI document, let’s set up the basics.
If at any point throughout this post you are in doubt or you require some additional clarity, then please refer to the specification document.
NOTE: I’ve used version
3.0.3
for my examples.
Basic Structure
An OpenAPI document is written in YAML or JSON format (I’ve used YAML for my examples).
It consists of various sections that collectively describe your API.
At a high level, here’s what the structure looks like:
openapi: 3.0.3
info:
title: Your API
version: 1.0.0
paths: {}
components: {}
The above example is not exhaustive as it only describes three ‘objects’:
info
: Info Objectpaths
: Paths Objectcomponents
: Components Object
Refer to the OpenAPI Object for a complete list of top-level (i.e. root object) fields.
Defining Endpoints
Now, let’s break down the process of defining endpoints in your OpenAPI document.
Paths and Methods
Endpoints are defined using the paths
section. Each endpoint is associated with an HTTP method (e.g., GET, POST) and a URL path. Here’s an example:
paths:
/users:
get:
summary: Get a list of users
responses:
'200':
description: Successful response
REF: Path Item Object.
Parameters
You can add parameters to your endpoints using the parameters
section. Parameters can be path parameters, query parameters, headers, and more. Here’s an example of a path parameter:
paths:
/users/{userId}:
parameters:
- name: userId
in: path
required: true
schema:
type: integer
Possible values for the in
field are: “query”, “header”, “path” or “cookie”.
REF: Parameter Object.
Structuring Data
Defining request and response bodies, along with data types, is crucial for a well-documented API.
Request and Response Bodies
You can specify request and response bodies using the requestBody
and responses
sections. Here’s how to define a request body:
paths:
/users:
post:
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
The use of $ref
allows us to avoid having an inline schema for one that is defined separately from the path object. This is useful in scenarios where the referenced schema might need to be reused across different paths. We’ll take a look at the components/schemas
section next.
REF: Operation Object and Request Body Object.
Data Types
Data types are defined under the components/schemas
section.
Here’s an example of defining a User
schema:
components:
schemas:
User:
type: object
properties:
id:
type: integer
username:
type: string
The components
section not only supports defining schemas
separate from where they should be referenced but also responses
, parameters
, examples
and more. We’ll take a look at some of these fields in more detail later.
REF: Components Object.
Adding Metadata
Enhance your API documentation by adding metadata and grouping related endpoints.
API Information
The info
section provides high-level information about your API, such as title, version, and description:
info:
title: Your API
version: 1.0.0
description: This is a sample API documentation.
REF: Info Object.
Tags and Grouping
You can group related endpoints using tags (typically added within the Operation Object):
tags:
- name: Users
description: Operations related to users
But tags can also be defined at the top-level (i.e. root object).
For example, Fastly uses the following conventions for its tags which determine how endpoints are documented on Fastly’s Developer Hub (DevHub):
tags:
- name: unlisted # Publish on DevHub at an unlisted URL and exclude from search results
- name: excludeFromSearch # Publish on DevHub but exclude from search results
- name: internal # Do not publish on DevHub or build into API clients
- name: beta # Display "beta" notice on DevHub
- name: limited-availability # Display "LA" notice on DevHub
REF: Tag Object.
Handling Errors
Communicating errors is crucial in API design, and OpenAPI helps you to define your error responses.
Status Codes
Specify status codes and their meanings in your responses
section:
paths:
/users/{userId}:
get:
responses:
'200':
description: Successful response
'404':
description: User not found
The responses
field is a container for the expected responses of an operation. The container maps a HTTP response code (e.g. 200
or 404
etc) to the expected response.
REF: Responses Object.
Error Responses
You can also define error responses with detailed information, and for specific response types:
responses:
'400':
description: Invalid Credit
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
In the above example we describe a 400 Bad Request
error response that will have the Content-Type
of application/json
(i.e. the response will use JSON) and we reference an external schema.
Below is an example schema definition that uses the popular “Problem Details” format from RFC 7807:
components:
schemas:
Error:
type: object
properties:
type:
type: string
description: A URI reference that identifies the problem type.
example: "https://example.com/probs/out-of-credit"
title:
type: string
description: A short, human-readable summary of the problem.
example: "You do not have enough credit."
status:
type: integer
description: The HTTP status code generated by the origin server for this occurrence of the problem.
example: 400
detail:
type: string
description: A human-readable explanation specific to this occurrence of the problem.
example: "Your current balance is 30, but that costs 50."
instance:
type: string
description: A URI reference that identifies the specific occurrence of the problem.
example: "/account/12345/msgs/abc"
required:
- type
- title
- status
What this schema describes is the following example error JSON that a user might see:
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
REF: Response Object.
Testing and Validation
Ensure the reliability of your API by testing and validating it using OpenAPI tools.
Tools for Validation
OpenAPI supports validating your API against the defined schemas using a variety of tools, such as Swagger Inspector and Spectral.
Generating Client SDKs
Using OpenAPI also helps with generating (and maintaining) client SDKs by using tools like Swagger Codegen and OpenAPI-Generator to accelerate development for various programming languages.
I’ve written about this process on the Fastly dev.to blog:
Better Fastly API clients with OpenAPI Generator
Conclusion
Embracing OpenAPI as a core component of your API development process can lead to more efficient, well-documented, and collaborative projects.
By understanding the key sections of an OpenAPI document and leveraging its capabilities, you can create APIs that are not only robust but also user-friendly.
Whether you’re a solo developer or part of a large team, OpenAPI is a powerful tool that simplifies API design and documentation, ultimately contributing to a better developer experience.
In this brief guide, I’ve explored the importance of OpenAPI in API development, discussed its benefits, and walked you through the process of writing an OpenAPI document.
By breaking down the essential sections and providing practical examples, I hope you feel empowered to leverage OpenAPI for your next API project. Happy API designing!