Especificar un servicio API con open API 3.1
Artículo de Isabel Arnaiz
¿Qué es Open API 3.1?
Open API 3.1 es la versión más reciente del estándar Open API, diseñado para describir servicios API basados en el protocolo HTTP. Permite comprender y consumir un servicio API sin acceder a su código fuente, facilitando la generación de documentación interactiva y la automatización del API testing. Es legible tanto para humanos como para computadoras y es ampliamente utilizado en la industria.
Si ya estás familiarizado con el enfoque API first y reconoces el valor de planificar y describir tu servicio API a través de un fichero de especificación, entonces es, sin lugar a duda, el momento adecuado para presentar los elementos que no pueden faltar en este tipo de documentos.
Utilicemos para ello el estándar de especificación Open API (OAS) en su versión más reciente, la Open API 3.1.
Por si no estás familiarizado con Open API, se trata de un estándar para la descripción de servicios API basados en el protocolo HTTP, el cual permite comprender y consumir el servicio sin necesidad de acceder a su código fuente, documentación adicional o inspeccionar el tráfico de red. Es además legible por humanos y computadoras, facilitando la generación de documentación interactiva y la automatización de casos de prueba.
Hoy en día, OpenAPI se posiciona como el estándar más utilizado en la industria para describir APIs.
Dicho esto, ¡comencemos!
Acerca del formato de especificación en open API
JSON o YAML?
Antes que nada hay que aclarar que la especificación Open API puede seguir un formato JSON o YAML indistintamente. La selección de uno u otro queda a tu preferencia.
Los siguientes ejemplos serán descritos con el formato YAML, pues visualmente los caracteres de llaves tienden a sobrecargar el documento de especificación cuando existen muchos objetos anidados, dificultando así la comprensión de la estructura de datos. Pero repetimos: ¡es tu decisión qué formato utilizar!
¿Qué no puede faltar en un fichero de especificación de open API?
En primer lugar, el objeto Open API, el cual constituye la raíz del fichero. Este objeto incluye a su vez cuatro propiedades fundamentales:
#Estructura del objeto Open API
openapi: 3.1.0
info:
version: '1.0.0'
title: 'RedsauceBlog REST API'
servers:
- url: 'localhost:3000'
paths:
...
...
components:
...
…
security:
...
La propiedad openapi permite establecer la versión de Open API con la que se construye el documento. Definir el valor de esta propiedad es fundamental, pues las versiones del estándar OAS difieren en algunos aspectos, haciendo indispensable declarar la versión específica con que se trabaja.
Adicionalmente, se deben asociar al objeto OpenAPI las propiedades info y server. La primera permite establecer el título y versión de la API que se especifica. La segunda permite definir el dominio o dominios donde se desplegará el servicio. Si esta última no se define o es una colección vacía, entonces toma el valor por defecto “/”.
1. Las rutas en openAPI
La propiedad paths es una parte vital del documento; en otras palabras es el “núcleo” de la especificación. En ella se definen cada una de las rutas relativas que conforman la API, las cuales se concatenan a la ruta base definida en la propiedad server para construir la ruta absoluta.
Supongamos que queremos crear un documento de especificación para una API que se compone de las siguientes operaciones:
Operación | Ruta | Funcionalidad |
---|---|---|
post | /user | Crear un nuevo usuario |
get | /user/:id | Devolver los datos del usuario |
delete | /user/:id | Eliminar un usuario |
En este ejemplo, tenemos dos rutas fundamentales a incluir en el fichero de especificación: la ruta /user y la ruta /user/:id.
Ten en cuenta que cada ruta se especifica a través de un objeto cuyas propiedades incluyen los verbos asociados a las distintas operaciones: get, post, update y delete, entre otros. Por tanto, dentro de cada una de estas propiedades de operación podrás detallar los encabezados, cuerpos de petición, tipos de respuesta y otros elementos de cada operación que incorpore la ruta.
Así mismo, los parámetros de una ruta, ya sean de tipo “path” o “query”, deben ser especificados en la propiedad parameters de este mismo objeto en forma de colección, siendo posible describir su esquema de datos en esta sección del documento o en la propiedad components tal y como explicaremos más adelante.
Observa cómo quedaría el archivo de especificación para las tres operaciones descritas en la tabla y cómo debes definir el parámetro “id” de tipo “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
# Primera operación de la ruta '/user/:id'
get:
summary: Returns details about a particular user
operationId: getUser
responses:
...
...
# Otra operación de la ruta '/user/:id'
delete:
summary: Deletes a particular user
operationId: deleteUser
responses:
...
...
Para las operaciones que requieren enviar información en el cuerpo de la petición es necesario especificar el esquema de datos. Siguiendo con esta sencilla API de ejemplo, para la operación "Crear un usuario" resulta necesario enviar al servidor los datos de dicho usuario, como mínimo su nombre, email de registro y contraseña. Veamos entonces cómo definir el cuerpo de la solicitud para la operación post(user/:id):
...
paths:
/user:
post:
summary: Create a new user
operationId: createUser
requestBody:
schema:
# Objeto a enviar en el cuerpo de la petición
type: object
required:
- email
- name
- password
properties:
email:
type: string
format: email
name:
type: string
password:
type: string
responses:
...
...
/user/{id}:
...
Resulta importante destacar que los tipos de datos y formatos que se declaran en la especificación de una estructura son consecuentes con el estándar JSON Schema Specification Draft 2020-12. Esta es una de las actualizaciones de la versión más reciente del estándar Open API que difiere de versiones anteriores.
2. Definir la respuesta en openAPI
Como mínimo, para cada operación debe incluirse una respuesta exitosa y una respuesta de error. Así mismo, si se devuelve información en el cuerpo de la respuesta, resulta necesario especificar el tipo de contenido de la misma en una o varias de las alternativas disponibles, como por ejemplo application/json, text/plain, multipart/form-data u otro.
...
paths:
/user:
post:
summary: Create a new user
operationId: createUser
requestBody:
...
responses:
# Respuesta exitosa
'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
# Respuesta de error
'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}:
...
3. Los componentes en openAPI
En la propiedad components puedes definir todos los esquemas de datos, parámetros, encabezados, cuerpos de petición, ejemplos, mecanismos de seguridad, entre otros elementos que se utilizan a lo largo del documento de especificación. Si deseas explorar la lista completa de opciones presiona en el siguiente enlace..
Veamos cómo especificar diferentes elementos en la propiedad “components”:
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
Cada declaración de un esquema en particular debe ser iniciado con un nombre descriptivo y único. Por supuesto, los esquemas de cada elemento son diferentes en cuanto a sus propiedades, por lo que es necesario revisar su composición en la documentación oficial de OAS para evitar errores.
Tu mejor amigo: $ref
$ref es sin duda tu mejor aliado a la hora de reducir la complejidad y facilitar la comprensión de tu especificación.
Con $ref puedes hacer referencia a la definición de un elemento que se encuentra alojado dentro o fuera del documento principal de la especificación con solo utilizar las declaraciones “local”, “remoto” o “externo”. Observa cómo queda la sintaxis para referenciar un componente a diferentes alcances:
Alcance | |
---|---|
local | $ref: '#/components/definitions/myElement' |
remoto | $ref: 'documentOrigin/documentName#/myElement' |
externo | $ref: 'baseURL/documentName#/myElement' |
De esta forma, puedes referenciar los esquemas de los elementos que definas, bien sea en el objeto components o en un fichero independiente, en cada una de las secciones que le correspondan del documento principal de especificación.
Aprovechando las facilidades de $ref no solo haces visualmente más organizado y entendible tu documento, sino que puedes reducir su número de líneas al separar los elementos en ficheros independientes.
Veamos cómo referenciar en el esquema de una respuesta, un objeto que ha sido definido previamente en los componentes.
...
paths:
...
/user/{id}:
parameters:
# referencia al parámetro id definido en components
- $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:
# referencia a esquema User definido en components
$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:
...
4. La seguridad en openAPI
Por lo general, la mayoría de los servicios API requieren de uno o varios mecanismos de seguridad para al menos una operación. Por tanto, es vital incluir la propiedad security del objeto OpenAPI en tu documento y comprender su correcta utilización.
En esencia, la propiedad security permite definir una colección que incluye diferentes mecanismos de seguridad (HTTP authentication, API key, mutual TLS, OAuth2, OpenID Connect Discovery) con alcance global para todas las operaciones.
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':
...
Es importante destacar que estos mecanismos deben ser previamente definidos en components y que, adicionalmente, cada operación incluye una propiedad security para redefinir la colección de mecanismos. De esta forma, si necesitas anular o modificar el alcance de uno de ellos en una operación particular, sólo debes redefinir su colección.
Recuerda además que solo uno de los requisitos de seguridad debe cumplirse para autorizar una solicitud y que en caso de que una operación no requiera mecanismo de seguridad debes declarar su colección como vacía.
No olvides los ejemplos en tus ficheros open API
No existe una buena especificación de API sin ejemplos en sus esquemas y respuestas. Los ejemplos pueden especificarse o bien a través de la propiedad examples disponible para cada esquema y respuesta, o como parte del objeto components. Por supuesto, si utilizas esta segunda alternativa debes posteriormente referenciar el ejemplo en la sección del documento que corresponda.
Destacar que la propiedad examples es una colección para la versión Open API 3.1, por lo que es necesario iniciar cada ejemplo con un guión.
# Colección de ejemplos declarados en el esquema del elemento
...
components:
parameters:
id:
name: id
in: path
description: Id of the user
required: true
schema:
type: integer
format: int32
examples:
- 1
- 2
...
# Ejemplos declarados como parte del objeto components…
...
components:
parameters:
...
schema:
...
examples:
BadRequestError:
value:
code: 400
message: Bad Request Error
# …pueden ser referenciados luego:
...
responses:
...
'400':
description: Bad request error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
$ref: '#/components/examples/BadRequestError'
'500':
...
Los ejemplos son esenciales para el desarrollo de la documentación y entornos de simulación, así que no prescindas nunca de ellos.
Y no olvides aprovechar las propiedades summary y description, disponibles para la mayor parte de los objetos definidos por el estándar OAS. Estas dos propiedades permiten describir las características de cada uno de ellos y resultan muy útiles para la generación automática de documentación.
¡Estás listo para especificar APIs bajo el Open API 3.1!
Estos son los indispensables para el desarrollo de un fichero de especificación de API basado en OAS.
Un bonus
Una buena práctica es incluir el uso de las etiquetas. Estos metadatos te permiten organizar tus operaciones en diferentes grupos con funcionalidades o acciones afines , tipos de permisos o cualquier otra característica compartida.
Para ello, puedes definir un grupo global de etiquetas en la propiedad tags del objeto OpenAPI y después asignar a cada operación aquellas etiquetas que la caracterizan.
¡Y ahora sí lo tienes todo para especificar tus futuras APIs bajo el Open API 3.1!
¿Te animas a usarlo en tu próximo archivo de descripción? Decarga este consejo de Redsauce y muchos más en nuestro ebook: