Redsauce's Software QA Blog

How to describe an API service with openAPI 3.1

Posted by Isabel Arnaiz

{1570}

What is OpenAPI 3.1?

OpenAPI 3.1 is the latest version of the OpenAPI standard, designed to describe API services based on the HTTP protocol. It allows understanding and consuming an API service without accessing its source code, facilitating the generation of interactive documentation and test automation. It is readable by both humans and computers and is widely used in the industry.


If you’ve already heard about the API-first approach and you recognize the value of planning and describing your API service through a specification file,

then it's undoubtedly the best time to talk about which elements are necessary for this type of document.


To this purpose, let's use the OpenAPI specification standard (OAS) in its latest version: OpenAPI 3.1.


In case you are not familiar with OpenAPI, it is a standard for the description of API services based on the HTTP protocol, which allows you to understand and consume the capabilities of an API service without having access to its source code, additional documentation or having to inspect the network traffic. In addition, OpenAPI is readable by both humans and computers, making the generation of interactive documentation and the automation of test scenarios much easier.


Nowadays, OpenAPI is positioned as the most popular standard in the industry to describe APIs.


That being said, let's get started!

About the specification format in openAPI

JSON o YAML?


First of all, let's make it clear that the specification code based on OAS can be implemented in any of two possible alternatives, JSON or YAML format. Which one of them you use in your code is up to you.

The following examples will be described in YAML format because curly brace characters tend to visually clutter the specification document when there are many nested objects, thus making it difficult to understand the data structure. But once again: it's your decision which format to use!

What must be included in an openAPI specification file?

First, the OpenAPI object, which is the root of the file. This object includes four important properties:

#OpenAPI object

openapi: 3.1.0
info:
version: '1.0.0'
title: 'RedsauceBlog REST API'
servers:
- url: 'localhost:3000'
paths:
...
...
components:
...
...
security:
...

The openapi property allows you to set the value of the OAS version you are using in your document. Defining the value of this property is non-negotiable since different versions of the OAS standard differ among them in specific aspects, so syntax errors may arise if the version and the structure of the document do not match.


Download free QA ebook


Additionally, you must define values for the info and server properties in the OpenAPI object. The first allows you to set the title and version of the API that is specified. The second allows defining the domain or domains where the service will be deployed. If the latter is not defined or is an empty collection, then it defaults to "/".

1. The routes in openAPI

The paths property is the main part of the document; in other words, it is the "core" of the specification. This property defines all the routes that make up the API which can be concatenated with the base route defined in the server property to build the absolute route.


Suppose we want to create a specification document for an API that is made up of the following operations:

Operation

Rute

Functionality

post

/user

Create a new user

get

/user/:id

Return the user data

delete

/user/:id

Delete an user

In this example, there are two fundamental paths include in the document: the "/user" and the "/user/:id" routes.


Take into account that each route is specified through an object whose properties include the actions associated with the different operations: get, post, update and delete, among others. Therefore, within each of these operation properties you will be able to detail the headers, request bodies, response types and other elements of each operation that incorporates the route.


Likewise, the parameters of a route, whether they are of the "path" or "query" type, must be specified in the parameters property of this same object in the form of a collection, and it is possible to describe its data schema in this section of the document or in the components property as we will explain later.


Just look at how the specification file would look like for the three operations described in the table and how you should define the “id” parameter of type “path”.

...
paths:
/user:
post:
summary: Create a new user
operationId: createUser
responses:
...
...
/user/{id}:
parameters:
- name: id
in: path
description: The user´s id value
required: true
schema:
type: integer
format: int32
# one operation for the '/user/:id' path
get:
summary: Returns details about a particular user
operationId: getUser
responses:
...
...
# another operation for the '/user/:id' path
delete:
summary: Deletes a particular user
operationId: deleteUser
responses:
...
...

For operations that require sending information in the request body, then it is necessary to specify their data scheme. Continuing with our example API, for the "Create a user" operation it is necessary to send the user's data to the server, at least their name, registration email and password. So, let's see how to define the request body for the post(user/:id) operation:

...
paths:
/user:
post:
summary: Create a new user
operationId: createUser
requestBody:
schema:
# request body object
type: object
required:
- email
- name
- password
properties:
email:
type: string
format: email
name:
type: string
pasword:
type: string
responses:
...
...
/user/{id}:
...

Note that data types and formats declared in each required schema are consistent with the JSON Schema Specification Draft 2020-12 standard. This is one of the updates of the latest version of OpenAPI that differs from the previous versions.

2. A response must be defined in openAPI

At the very least, one successful response and one error response must be included in every operation. Likewise, if information is returned in the response body, it is necessary to specify the media type of the response in one or more of the available alternatives, such as: application/json, text/plain, multipart/form-data, or other.

...
paths:
/user:
post:
summary: Create a new user
operationId: createUser
requestBody:
...
responses:
# a successful response
'201':
description: User was successfully created
content:
application/json:
schema:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type:
- string
- null
# an error response
'400':
description: Bad request error
content:
application/json:
schema:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
# Respuesta negativa
'500':
description: Internal server error
content:
application/json:
schema:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
/user/{id}:
...

Components in openAPI

In the components property you can define all the data schemas, parameters, headers, request bodies, examples, security mechanisms, among other elements that are used throughout the specification document. If you want to explore the full list of options click on the next link.


Let's see how to specify different elements in the components property:

components:
parameters:
id:
name: id
in: path
description: Id of the user
required: true
schema:
type: integer
format: int32
schemas:
NewUser:
type: object
required:
- email
- name
- password
properties:
email:
type: string
format: email
name:
type: string
pasword:
type: string
User:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type:
- string
- null
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
securitySchemes:
BearerToken:
type: http
scheme: bearer
bearerFormat: JWT

Each declaration of a particular schema must be started with a descriptive and unique name. Of course, the schemas of each element are different in terms of their properties, so it is necessary to review their composition in the official OAS documentation to avoid errors.


Your best friend: $ref


$ref is undoubtedly your best ally when it comes to reducing complexity and making your specification easier to understand.

With $ref you can reference the definition of an element that is hosted inside or outside of the main specification document by simply using the “local”, “remote” or “external” declarations. Take a look at the syntax for referencing a component at different scopes:

Scope


local

$ref: '#/components/definitions/myElement'

remote

$ref: 'documentOrigin/documentName#/myElement'

extern

$ref: 'baseURL/documentName#/myElement'

In this way, you can reference the schemas of the elements you define (either in the components object or in a separate file) in each of the corresponding sections of the main specification document.


Taking advantage of the ease of using $ref not only makes your document more visually organized and understandable, but you can also reduce the number of lines by separating the elements into independent files.


Let's see how to use a reference in the schema of a response to an object that has been previously defined in the components.

...
paths:
...
/user/{id}:
parameters:
# `id` parameter was defined in components and it is referenced at this line
- $ref: '#/components/parameters/id'
get:
summary: Returns details about a particular user
operationId: getUser
responses:
'200':
description: Details about a user by id
content:
application/json:
schema:
# User schema was also defined in components and it is used next
$ref: '#/components/schemas/User'
'401':
description: 'Unauthorized'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
...

What about security in openAPI?

In general, most API services require one or more security mechanisms for at least one operation. Therefore, it is vital to include the security property of the [OpenAPI object](https://spec.openapis.org/ oas/v3.1.0#openapi-object) in your document and understand its proper use.


The security property allows defining a collection of different security mechanisms (HTTP authentication, API key, mutual TLS, OAuth2, OpenID Connect Discovery) with global scope for all operations.

openapi: 3.1.0
...
security:
- BearerToken: []
paths:
/user:
post:
summary: Create a new user
operationId: createUser
security: []
requestBody:
...
responses:
'201':
...
'400':
...
'500':
...
/user/{id}:
parameters:
- $ref: '#/components/parameters/id'
get:
summary: Returns details about a particular user
operationId: getUser
responses:
'200':
...
'401':
...
'500':
...

It’s worth mentioning that these mechanisms must be previously defined in the components property and, additionally, each operation object must include a security property to redefine the collection of mechanisms. So if you need to override or modify the scope of one of them for a particular operation, you just have to redefine its collection.


Also be aware that only one of the security requirements must be met to authorize a request and that in case an operation does not require a security mechanism you must declare its collection as empty.

Don't forget the examples in your openAPI files.

There is no good API specification without examples in its schemas and answers. Examples can be specified either through the examples property available for each schema and response, or as part of the components object. Of course, if you use this second alternative, you must later reference the example in the corresponding section of the document.


Note that the examples property is a collection in OpenAPI version 3.1, so it is necessary to start each example with a hyphen.

 # Example array declared in the inner schema of a random element
...
components:
parameters:
id:
name: id
in: path
description: Id of the user
required: true
schema:
type: integer
format: int32
examples:
- 1
- 2
...
 # Examples previously declared in components object…
...
components:
parameters:
...
schema:
...
examples:
BadRequestError:
value:
code: 400
message: Bad Request Error

# …can be referenced later as:
...
responses:
...
'400':
description: Bad request error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
$ref: '#/components/examples/BadRequestError'
'500':
...

Examples are absolutely necessary for documentation and simulation environments, so you must never do without them.


And don't forget to take advantage of the summary and description properties, both available for most of the objects defined by the OAS. These two properties are really helpful for describing the relevant issues of an object and they are very useful for the automatic generation of documentation.

You are ready to specify APIs under OpenAPI 3.1!

These are the essential steps to developing an OAS-based API specification document.


A bonus


A good practice is to include the use of tags. This metadata allows you to organize your operations into different groups with related actions, functionalities, types of permissions, or any other shared feature. To put this in practice, you can define a global group of tags in the tags property and then, you must assign each tag property to operations that can be characterized by it.


And now you have everything you need to describe your future APIs under the OpenAPI 3.1!


So, will you use it in your next API description document?


image

About us

You have reached the blog of Redsauce, a team of experts in QA and software development. Here we will talk about agile testing, automation, cybersecurity… Welcome!