Documentación auto-generada con Spring REST Docs

Avanzado
Spring Boot
Spring Boot
Actualizado: 07/05/2026

Diagrama: tutorial-spring-boot-spring-rest-docs

El problema de Swagger sin verificación

springdoc-openapi genera un swagger-ui.html precioso a partir de las anotaciones del controlador. El equipo de frontend lo consume, los partners externos lo usan como referencia y, durante meses, todo va bien. Hasta que alguien renombra un campo del DTO de salida sin tocar la anotación @Schema. Los tests pasan, el build pasa, el Swagger sigue mostrando el campo viejo. El partner integra contra esa documentación y el día del despliegue su cliente falla.

OpenAPI generado por anotaciones tiene una desconexión real con el comportamiento. La firma del método y los DTOs se reflejan, pero los detalles (mensaje de error exacto, formato de fecha, ejemplos válidos, headers que devuelve) se inventan o se quedan obsoletos.

Spring REST Docs invierte el modelo: la documentación se genera desde tests que efectivamente se ejecutan. Si el test no llama al endpoint o no verifica la respuesta, no hay snippet. Si el contrato cambia, el test falla, el snippet no se regenera y el build rompe.

Cómo funciona Spring REST Docs

Spring REST Docs es un módulo de Spring que se activa en tests de integración. Durante la ejecución, genera fragmentos (snippets) Asciidoctor por cada llamada documentada:

  • curl-request.adoc, httpie-request.adoc: ejemplos para copiar.
  • http-request.adoc, http-response.adoc: petición y respuesta crudas.
  • path-parameters.adoc, query-parameters.adoc, request-headers.adoc.
  • request-fields.adoc, response-fields.adoc: tabla de campos con tipo y descripción.
  • response-body.adoc, request-body.adoc: cuerpos completos.

Esos snippets se incluyen desde un fichero principal index.adoc que el plugin de Asciidoctor convierte a HTML.

graph LR
    A[Test JUnit 6 + MockMvc] --> B[document called]
    B --> C[Snippets adoc generados]
    C --> D[index.adoc include]
    D --> E[asciidoctor maven plugin]
    E --> F[index.html navegable]

Setup con Maven

<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <version>3.1.0</version>
    <scope>test</scope>
</dependency>

<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <version>3.0.0</version>
    <executions>
        <execution>
            <id>generate-docs</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>
                <backend>html</backend>
                <doctype>book</doctype>
                <sourceDirectory>src/main/asciidoc</sourceDirectory>
                <attributes>
                    <snippets>${project.build.directory}/generated-snippets</snippets>
                </attributes>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-asciidoctor</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>
</plugin>

El atributo snippets apunta a la carpeta donde Spring REST Docs deposita los .adoc. Asciidoctor los lee de allí cuando ensambla index.html.

Primer test que genera snippets

@WebMvcTest(EmpleadoController.class)
@AutoConfigureRestDocs(outputDir = "target/generated-snippets")
class EmpleadoControllerDocsTest {

    @Autowired
    private MockMvc mvc;

    @MockitoBean
    private EmpleadoService service;

    @Test
    void buscar_empleado_por_id() throws Exception {
        when(service.buscarPorId(1L))
            .thenReturn(new EmpleadoDto(1L, "Ana", "ana@empresa.com"));

        mvc.perform(get("/api/v1/empleados/{id}", 1L)
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andDo(document("empleados-buscar-por-id"));
    }
}

El parámetro de document(...) define la subcarpeta dentro de target/generated-snippets/. Tras ejecutar el test, esa carpeta contiene los snippets básicos. Si el cuerpo de la respuesta cambia, el snippet http-response.adoc también, y el HTML final reflejará la verdad.

Documentar campos de petición y respuesta

Sin descriptors, los snippets contienen JSON pero no descripciones. Lo idiomático es documentar campo a campo con tipo y descripción:

@Test
void crear_empleado() throws Exception {
    var dto = new EmpleadoCrearDto("Ana", "ana@empresa.com", 28000);

    mvc.perform(post("/api/v1/empleados")
            .contentType(MediaType.APPLICATION_JSON)
            .content(json.writeValueAsString(dto)))
        .andExpect(status().isCreated())
        .andDo(document("empleados-crear",
            requestFields(
                fieldWithPath("nombre").type(JsonFieldType.STRING)
                    .description("Nombre del empleado, no nulo y entre 2 y 60 caracteres."),
                fieldWithPath("email").type(JsonFieldType.STRING)
                    .description("Email único en el sistema."),
                fieldWithPath("salarioBruto").type(JsonFieldType.NUMBER)
                    .description("Salario bruto anual en euros, debe ser positivo.")
            ),
            responseFields(
                fieldWithPath("id").type(JsonFieldType.NUMBER)
                    .description("Identificador asignado por el sistema."),
                fieldWithPath("nombre").type(JsonFieldType.STRING).description("Nombre persistido."),
                fieldWithPath("email").type(JsonFieldType.STRING).description("Email persistido."),
                fieldWithPath("createdAt").type(JsonFieldType.STRING)
                    .description("Fecha de creación en formato ISO-8601 (UTC).")
            ),
            responseHeaders(
                headerWithName("Location").description("URI absoluta del recurso creado.")
            )
        ));
}

Si el JSON real contiene un campo no documentado, el test falla con Fields with the following paths were not found. Esto fuerza a mantener el contrato sincronizado: añadir un campo a la respuesta sin documentarlo rompe el build.

Documentar path variables y query params

@Test
void buscar_con_filtros() throws Exception {
    mvc.perform(get("/api/v1/empleados/{id}/contratos", 5L)
            .param("estado", "ACTIVO")
            .param("desde", "2026-01-01"))
        .andExpect(status().isOk())
        .andDo(document("empleados-contratos-buscar",
            pathParameters(
                parameterWithName("id").description("Identificador del empleado.")
            ),
            queryParameters(
                parameterWithName("estado").description("Estado del contrato. Valores: ACTIVO, FINALIZADO, PENDIENTE."),
                parameterWithName("desde").description("Fecha mínima de inicio en formato ISO-8601.")
            )
        ));
}

Cada elemento queda en su propio snippet (path-parameters.adoc, query-parameters.adoc) listo para incluir desde el documento principal.

Constraints automáticos desde Bean Validation

Si los DTOs llevan anotaciones de jakarta.validation (@NotBlank, @Size, @Email), Spring REST Docs las puede leer y añadir como atributos del campo:

public record EmpleadoCrearDto(
    @NotBlank @Size(min = 2, max = 60) String nombre,
    @Email String email,
    @Positive BigDecimal salarioBruto
) {}
var constraints = new ConstraintDescriptions(EmpleadoCrearDto.class);

requestFields(
    fieldWithPath("nombre").type(STRING)
        .description("Nombre")
        .attributes(key("constraints").value(constraints.descriptionsForProperty("nombre"))),
    ...
);

En el index.adoc, una plantilla custom para request-fields.adoc muestra esa columna. La documentación queda sincronizada con la validación real, sin duplicar las reglas.

El documento Asciidoctor

El fichero src/main/asciidoc/index.adoc orquesta los snippets:

= API Empleados
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== Buscar empleado por ID

include::{snippets}/empleados-buscar-por-id/curl-request.adoc[]
include::{snippets}/empleados-buscar-por-id/http-response.adoc[]
include::{snippets}/empleados-buscar-por-id/response-fields.adoc[]

== Crear empleado

Crea un nuevo empleado.

=== Petición

include::{snippets}/empleados-crear/http-request.adoc[]
include::{snippets}/empleados-crear/request-fields.adoc[]

=== Respuesta

include::{snippets}/empleados-crear/http-response.adoc[]
include::{snippets}/empleados-crear/response-fields.adoc[]
include::{snippets}/empleados-crear/response-headers.adoc[]

Tras mvn package, el HTML aparece en target/generated-docs/index.html.

WebTestClient para WebFlux y endpoints reactivos

Para servicios WebFlux o tests con servidor real, se usa WebTestClient:

<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-webtestclient</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureWebTestClient
@AutoConfigureRestDocs
class FacturasDocsTest {

    @Autowired
    private WebTestClient client;

    @Test
    void listar_facturas() {
        client.get().uri("/api/v1/facturas")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .consumeWith(document("facturas-listar"));
    }
}

La API es idéntica al caso MockMvc: mismos descriptors, mismos snippets, misma plantilla.

Integración en CI

El flujo típico:

  1. mvn verify ejecuta los tests, genera snippets.
  2. asciidoctor-maven-plugin ensambla index.html durante prepare-package.
  3. Un job de CI publica target/generated-docs/ a:
    • GitHub Pages del propio repo (con peaceiris/actions-gh-pages).
    • Un bucket S3 estático servido por CloudFront.
    • El portal de desarrollador del API (Backstage, Stoplight).

Si los tests fallan o el plugin no encuentra un snippet referenciado en index.adoc, el build se rompe. Imposible publicar documentación inconsistente.

Comparación con OpenAPI

| Aspecto | OpenAPI con springdoc | Spring REST Docs | |---------|----------------------|------------------| | Origen de la documentación | Anotaciones en código | Tests de integración | | Riesgo de drift | Alto (anotaciones se quedan obsoletas) | Nulo (test falla si el contrato cambia) | | Ejemplos realistas | Inventados o vacíos | Capturados de respuestas reales | | Curva de aprendizaje | Baja | Media | | Dynamic try-it-out (UI) | Sí (Swagger UI) | No por defecto, requiere herramienta externa | | Generación de clientes | Sí (openapi-generator) | No directo, requiere exportar a OpenAPI primero | | Documentación de errores | Limitada | Snippets por escenario, tan rica como tests |

Lo idiomático en proyectos serios es combinar ambos: OpenAPI para la UI interactiva y la generación de clientes, Spring REST Docs para la documentación pública rigurosa que se publica a partners. El plugin restdocs-api-spec une los dos mundos generando OpenAPI desde los tests REST Docs.

Buenas prácticas

  • Un test de documentación por endpoint, separado de los tests funcionales. Los tests funcionales pueden ser muchos y rápidos; los de docs son uno por caso representativo y tienen una intención distinta.
  • Documentar siempre los códigos de error (400, 404, 409): un endpoint no es contractualmente completo sin sus respuestas de error tipificadas.
  • Capturar respuestas reales con responseFields, no inventarlas: el snippet http-response.adoc muestra el JSON tal cual lo emitió el controlador, sin posibilidad de divergir.
  • Usar OperationPreprocessors para sanitizar headers sensibles (Authorization, Cookie) antes de incluirlos en la docs pública.
  • Versionar el index.adoc y considerarlo parte del código: revisarlo en code review junto con los tests.
  • Publicar la doc en cada release desde CI, no manualmente. Etiquetar la versión en la URL (/api/empleados/v1.4.0/) facilita el soporte cuando un partner integra contra una versión antigua.
  • Combinar con restdocs-api-spec para exportar a OpenAPI 3.1 cuando el equipo necesite Swagger UI o generación de clientes.

Spring REST Docs introduce disciplina: cada endpoint público requiere un test de documentación. Esa disciplina paga rápidamente: la documentación de tu API deja de ser "lo que el código debería hacer" para convertirse en "lo que el código verificablemente hace". Para clientes B2B, partners y equipos de plataforma, esa diferencia vale más que cualquier UI interactiva.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Spring Boot

Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Configurar Spring REST Docs con MockMvc en Spring Boot 4 y JUnit 6. Generar snippets de petición y respuesta con descriptors tipados. Documentar campos request, response, path variables y query params. Ensamblar el documento Asciidoctor index.adoc y generar HTML con asciidoctor-maven-plugin. Integrar la generación en CI y publicar en GitHub Pages o un bucket S3.