Cómo documentar una API REST con Swagger implementada con Spring Boot

Escrito por picodotdev el , actualizado el .
java planeta-codigo
Enlace permanente Comentarios

Una API REST no está obligada a publicar una definición de su API, sin embargo, para quien deba usar API es muy útil disponer de su documentación para usarla correctamente y descubrir de qué endpoints se compone, métodos HTTP, cuales son sus parámetros, el esquema de los cuerpos de la petición y de los resultados, los tipos de los datos y sus formatos, los códigos de retorno devueltos, las cabeceras y su autenticación. OpenAPI permite definir la interfaz de una aplicación de forma agnóstica de la tecnología y lenguaje en el que se implementa, por otro lado Swagger a partir de esa definición permite generar una interfaz HTML con su documentación. La librería Springdoc junto con Spring Boot permite generar tanto la especificación de la API como la documentación simplemente añadiendo una dependencia y varias anotaciones en la implementación de la API.

Disponer de documentación es esencial para el desarrollo, también es el caso de tener que usar una API REST donde es necesario conocer que endpoints dispone la API, métodos HTTP, cuales son sus parámetros, el esquema de los cuerpos de la petición y de los resultados, los tipos de los datos y sus formatos, los códigos de retorno devueltos, las cabeceras y su autenticación.

GraphQL en sus especificaciones detallan además del protocolo define también una forma de exportar un esquema de la API y publicarlo junto con la misma que sirve como documentación. Una API REST que está basada más en convenciones y semántica del protocolo HTTP que en una especificación nada le obliga a proporcionar una especificación de la API. Aunque una API implemente HATEOAS e intente ser más autoexplicativa la documentación sigue siendo útil para explorar la API sin necesidad de realizar las peticiones.

No tener una especificación de la API es un inconveniente porque un cambio en la interfaz de la API puede provocar errores de compatibilidad, no tener su documentación para revisar la API dificulta su uso al implementar un cliente. No tener documentación es un inconveniente pero tener documentación no generada a partir del código fuente o de la especificación de la API también lo es porque la documentación corre el riesgo de no estar actualizada y sincronizada con la implementación en el código fuente. Además de quedar la documentación desactualizada respecto al código fuente requiere tiempo de mantenimiento que no se dedica a otras tareas.

Hay iniciativas y herramientas para suplir la carencia de las API REST de no obligar a proporcionar una especificación de la API REST y generar la documentación documentación a partir del código fuente. También es importante poder probar la API de forma sencilla, una de las formas más habituales de probar una API es que la documentación incluya el comando de la herramienta de línea de comandos curl por su sencillez ni requerimientos adicionales que tener el comando instalado en sistema para ejecutarlo.

Documentación de un API con OpenAPI, Swagger y Springdoc

OpenAPI trata de proporcionar una especificación para definir esquemas de APIs agnósticas de la tecnología y la implementación de las APIs. Definida la interfaz de la API es posible crear un cliente o servidor que cumpla esa API. La definición de la API incluye sus endpoints, métodos HTTP, cuales son sus parámetros, el esquema de los cuerpos de la petición y de los resultados, los tipos de los datos y sus formatos, los códigos de retorno devueltos, las cabeceras y su autenticación.

Por otro lado las herramientas de Swagger permiten generar la documentación a partir de la especificación de la API y si se desea generar una implementación básica inicial de cliente y servidor para diferentes lenguajes de programación. La documentación de Swagger no solo incluye información sino que permite probar la API directamente desde la documentación u obtener el comando curl a ejecutar desde la línea de comandos.

En una aplicación que implementa una API REST con Spring Boot la librería Springdoc permite generar de forma automática la especificación de la API que implementa el código publicándose en un endpoint, esta librería también genera la documentación de Swagger de la API en otro endpoint.

Otra forma de obtener la especificación de la API es mediante el plugin para Gradle de springdoc o utilizar imagen de Docker de Swagger UI para crear un servidor que aloje la documentación. También es posible descargar la última versión de Swagger UI en el directorio dist, cambiar el archivo index.html y reemplazar la URL https://petstore.swagger.io/v2/swagger.json por la especificación de OpenAPI deseada.

1
2
./gradlew generateOpenApiDocs

gradlew-generateOpenApiDocs.sh

El documento en formato JSON incluye de la definición de la API, es un documento con el fin de ser utilizado por alguna herramienta como Swagger UI que en su caso genera la documentación en formato HTML.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{
  "openapi": "3.0.1",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Generated server url"
    }
  ],
  "tags": [
    {
      "name": "message",
      "description": "the message API"
    }
  ],
  "paths": {
    "/message": {
      "get": {
        "tags": [
          "message"
        ],
        "summary": "Get all messages",
        "description": "Returns all messages",
        "operationId": "getAll",
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "message"
        ],
        "summary": "Adds a message",
        "description": "Add a message",
        "operationId": "add",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Message"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful operation"
          },
          "400": {
            "description": "Invalid data"
          },
          "409": {
            "description": "Already exists"
          }
        }
      }
    },
    "...": {
    }
  },
  "components": {
    "schemas": {
      "Message": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "text": {
            "type": "string"
          }
        }
      }
    }
  }
}
api-docs.json

La documentación en formato HTML de Swagger tiene el siguiente aspecto con la que además de obtener información sobre la API es posible ejecutar sus operaciones y obtener el comando curl para ejecutarlo desde la linea de comandos.

Documentación de Swagger UI de una API REST

Ejemplo de documentación REST con Spring Boot y Swagger

El siguiente ejemplo de Spring Boot implementa una pequeña API REST con un endpoint y varios métodos HTTP, uno para obtener un mensaje, otro para añadir un mensaje y otro para eliminar un mensaje. La API se define en un interfaz con las anotaciones tanto de Spring para REST como las anotaciones de Swagger para la definición de la API y documentación que al iniciar la aplicación permite generar la definición en formato OpenAPI.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.springrestswagger;

...

@Tag(name = "message", description = "the message API")
@RequestMapping("/message")
public interface MesssageApi {

	@Operation(summary = "Get all messages", description = "Returns all messages", responses = {
		@ApiResponse(responseCode = "200", description = "Successful operation", content = @Content(schema = @Schema(implementation = Message.class))) }
	)
	@GetMapping(value = "", produces = { "application/json" })
	ResponseEntity<List<Message>> getAll();

	@Operation(summary = "Get a message by id", description = "Return a message", responses = {
		@ApiResponse(responseCode = "200", description = "Successful operation", content = @Content(schema = @Schema(implementation = Message.class))),
		@ApiResponse(responseCode = "400", description = "Invalid id supplied"),
		@ApiResponse(responseCode = "404", description = "Message not found") }
	)
	@GetMapping(value = "/{id}", produces = { "application/json" })
	ResponseEntity<Message> getById(@Parameter(description = "Id of message to return", required = true) @PathVariable("id") Long id);

	@Operation(summary = "Adds a message", description = "Add a message", responses = {
		@ApiResponse(responseCode = "200", description = "Successful operation"),
		@ApiResponse(responseCode = "400", description = "Invalid data"),
		@ApiResponse(responseCode = "409", description = "Already exists") }
	)
	@PutMapping(value = "", produces = { "application/json" })
	ResponseEntity<Void> add(@Parameter(description = "Message to add", required = true) @RequestBody Message message);

	@Operation(summary = "Deletes a message by id", description = "Delete a message", responses = {
		@ApiResponse(responseCode = "200", description = "Successful operation"),
		@ApiResponse(responseCode = "400", description = "Invalid id supplied"),
		@ApiResponse(responseCode = "404", description = "Message not found") }
	)
	@DeleteMapping(value = "/{id}", produces = { "application/json" })
	ResponseEntity<Void> deleteById(@Parameter(description = "Id of message to delete", required = true) @PathVariable("id") Long id);
}
MessageApi.java

La implementación de la API simplemente guarda en un mapa los mensajes, en caso de que detecte una condición de error lanza una excepción con el código de estado definido en la API para la condición, en caso de que la operación sea correcta se ejecuta su funcionalidad y se devuelve el código de estado 200 y los datos solicitados en su caso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package io.github.picodotdev.blogbitix.springrestswagger;

...

@RestController
public class MessageController implements RestApi {

    private Map<Long, Message> messages;

    public RestApiController() {
        this.messages = new HashMap<>();
        this.messages.put(1l, new Message(1l, "Hello World!"));
        this.messages.put(2l, new Message(2l, "Welcome to Blog Bitix!"));
    }

    @Override
    public ResponseEntity<List<Message>> getAll() {
        List<Message> m = messages.entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList());
        return ResponseEntity.ok(m);
    }

    @Override
    public ResponseEntity<Message> getById(Long id) {
        if (!exists(id)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Message not found");
        }
        return ResponseEntity.ok(messages.get(id));
    }

    @Override
    public ResponseEntity<Void> add(Message message) {
        if (exists(message.getId())) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "Already exists");
        }
        if (message.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid data");
        }
        messages.put(message.getId(), message);
        return ResponseEntity.ok().build();
    }

    @Override
    public ResponseEntity<Void> deleteById(Long id) {
        if (!exists(id)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Message not found");
        }
        messages.remove(id);
        return ResponseEntity.ok().build();
    }

    private boolean exists(Long id) {
        return messages.containsKey(id);
    }
}
MessageController.java

Con los siguientes comandos de curl es posible probar los diferentes métodos de la API.

1
2
#!/usr/bin/env bash
curl -v http://localhost:8080/message
curl-get-all.sh
1
2
#!/usr/bin/env bash
curl -v http://localhost:8080/message/1
curl-get.sh
1
2
#!/usr/bin/env bash
curl -v -X PUT http://localhost:8080/message -H "Content-Type: application/json" --data '{"id": 3, "text": "Darkest Dungeon is a good game"}'
curl-put.sh
1
2
#!/usr/bin/env bash
curl -v -X PUT http://localhost:8080/message -H "Content-Type: application/json" --data '{"id": 1, "text": "Darkest Dungeon is a good game"}'
curl-put-conflict.sh
1
2
#!/usr/bin/env bash
curl -v -X DELETE http://localhost:8080/message/1
curl-delete.sh

Este ejemplo es suficiente, pero no cumple con todos los niveles de madurez de REST, el ejemplo de este otro artículo se puede comparar con el de este para ver las diferencias y conocer las ventajas e inconvenientes de implementar HATEOAS y HAL en una API REST.

Con la aplicación iniciada en en la URL http://localhost:8080/v3/api-docs por defecto se exporta especificación de la API en formato OpenAPI, en la URL http://localhost:8080/swagger-ui.html por defecto está la documentación de la API de Swagger generada por Springdoc. Con solo añadir las dependencias de Springdoc a la herramienta de construcción, en este caso Gradle, Spring Boot hace disponibles ambos endpoints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
plugins {
	id 'java'
	id 'application' 
	id 'org.springframework.boot' version '2.5.2'
	id 'com.github.johnrengelman.processes' version '0.5.0'
	id 'org.springdoc.openapi-gradle-plugin' version '1.3.2'
}

application {
	group = 'io.github.picodotdev.blogbitix.springrestswagger'
	version = '0.0.1-SNAPSHOT'
	sourceCompatibility = '11'
	mainClass = 'io.github.picodotdev.blogbitix.springrestswagger.Main'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation platform('org.springframework.boot:spring-boot-dependencies:2.5.2')

	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springdoc:springdoc-openapi-webmvc-core:1.5.9'
	implementation 'org.springdoc:springdoc-openapi-ui:1.5.9'
}
build.gradle

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: