At scale, how do you keep Spring REST Docs maintainable — reducing per-test boilerplate, keeping constraints/docs in sync, and what tradeoffs does the test-driven-docs approach carry?
answer
- default preprocessors via Customizer / configurer
- reusable FieldDescriptor lists + custom Snippets
- ConstraintDescriptions from Bean Validation
- override Mustache snippet templates
- tradeoff: docs coupled to maintained tests; not a machine contract
basics
~20 sCentralize preprocessors and URI config as defaults, extract reusable snippet/descriptor helpers, drive validation docs from Bean Validation via ConstraintDescriptions, and use snippet templates for consistency. The tradeoff: docs require and depend on maintained tests, and drift breaks the build (intentionally).
solid answer
~50 sThe failure mode at scale is copy-pasted document() calls and drifting hand-written .adoc. I standardize: (1) register default preprocessors and URI (prettyPrint, header allowlist, modifyUris/uriHost) once via a RestDocsMockMvcConfigurationCustomizer or the documentationConfiguration, so every test inherits clean output; (2) extract reusable FieldDescriptor lists and custom Snippet helpers (e.g. an errorFields() shared across endpoints); (3) generate constraint text from Bean Validation annotations with ConstraintDescriptions plus a custom snippet template, so validation docs can't drift from the DTO; (4) override default snippet templates (src/test/resources/org/springframework/restdocs/templates) for house style. The core tradeoff: REST Docs guarantees accuracy by coupling docs to tests — you must write and maintain a controller test per documented interaction, and any payload change fails the build until docs are updated. That's a feature for correctness but a real cost, and it doesn't produce a machine-readable contract (OpenAPI is a separate concern). I weigh that against annotation-based generators.
code
java · 21 lines// 1) Global defaults so no test repeats preprocessors/URIs (Boot)
@TestConfiguration
class RestDocsConfig {
@Bean
RestDocsMockMvcConfigurationCustomizer restDocsCustomizer() {
return configurer -> configurer
.operationPreprocessors()
.withRequestDefaults(modifyUris().scheme("https").host("api.example.com").removePort(),
removeHeaders("Host"))
.withResponseDefaults(prettyPrint());
}
}
// 2) Constraints straight from Bean Validation -> no drift
ConstraintDescriptions userConstraints = new ConstraintDescriptions(CreateUserRequest.class);
List<FieldDescriptor> fields = List.of(
fieldWithPath("name").description("Display name")
.attributes(key("constraints").value(userConstraints.descriptionsForProperty("name"))),
fieldWithPath("email").description("Email")
.attributes(key("constraints").value(userConstraints.descriptionsForProperty("email"))));
// requires a custom request-fields.snippet template rendering the 'constraints' attributego deeper
Aware that copy-pasting document() calls gets unwieldy and there are ways to share config.
Can register default preprocessors and extract shared descriptor lists.
Uses ConstraintDescriptions, custom snippet templates, and identifier namespacing; understands the test-coupling tradeoff.
Sets org-wide conventions, weighs REST Docs (accuracy, human docs) vs OpenAPI generators (contract, lower effort), and owns the build/publish pipeline.
**The scaling problem.** REST Docs is powerful but verbose: each documented interaction is a test with a `document(...)` call, descriptors, and preprocessors, plus a hand-authored narrative `.adoc` that `include::`s snippets. Without discipline you get duplicated preprocessor chains, repeated descriptor lists, and an ever-growing manual index file. **Techniques to keep it maintainable.** 1. **Centralize preprocessors & URIs.** Register defaults once so no test repeats them: - Manual: `documentationConfiguration(rd).operationPreprocessors().withRequestDefaults(modifyUris().host("api.example.com").removePort(), removeHeaders("Host")).withResponseDefaults(prettyPrint())`. - Boot: a `RestDocsMockMvcConfigurationCustomizer` (or `RestDocsWebTestClientConfigurationCustomizer`) bean, and/or `@AutoConfigureRestDocs(uriScheme="https", uriHost="api.example.com", uriPort=443)`. 2. **Reusable descriptors & custom snippets.** Extract shared `List<FieldDescriptor>` (e.g. pagination envelope, error body) into helper methods. Build custom `Snippet`s with `SnippetException`-safe templates for cross-cutting concerns; `snippets(...)` on the configurer can even add default snippets to every call. 3. **Constraints from Bean Validation.** `org.springframework.restdocs.constraints.ConstraintDescriptions` reads `jakarta.validation` annotations (`@NotNull`, `@Size`, `@Pattern`) off the request DTO. Combined with a custom `request-fields` template that renders a `constraints` attribute, validation rules are documented straight from the code — one source of truth, no drift. 4. **Snippet templates.** Default Mustache templates can be overridden by placing files under `src/test/resources/org/springframework/restdocs/templates/` (e.g. `response-fields.snippet`) to match house formatting, add columns, or localize. 5. **Narrative organization.** Split the `.adoc` by resource, use attributes for the snippets dir, and let each test own its identifier namespace (`{ClassName}/{methodName}` templating) to avoid collisions. **Tradeoffs — the principal-level judgment.** - **Accuracy vs. effort.** REST Docs's guarantee (docs come from passing tests; drift breaks the build) is its biggest strength and its cost — you must maintain a test per documented interaction, and it doesn't ease onboarding for teams unwilling to keep tests green. - **No machine-readable contract by itself.** It emits human docs (Asciidoctor/Markdown/HTML/PDF), not an OpenAPI schema or live contract — that's a sibling concern. Teams needing a consumable spec pair it with, or choose, a different tool. (Community bridges to OpenAPI exist but are out of this leaf's scope.) - **Coupling to test harness.** Docs quality depends on test coverage; endpoints without tests get no docs — which can be desirable (forces testing) or a gap. - **Build integration.** The Asciidoctor plugin must run after tests; snippet dir wiring, PDF/HTML outputs, and publishing add pipeline complexity. **When to choose it.** Pick REST Docs when documentation *accuracy* is paramount, you already test controllers thoroughly, and human-readable docs (not a machine contract) are the goal. Reconsider when you need a generated OpenAPI contract as the primary artifact or the team can't sustain test-per-endpoint discipline. **Gotchas at scale.** - Per-call preprocessors *replace* defaults — teams sometimes lose their global masking by passing a local preprocessor. Document the convention. - Shared descriptor lists must stay in sync with actual payloads or strict validation fails broadly — a single envelope change ripples. - Overridden templates are global; test them.
- How do you keep validation documentation from drifting from the DTO?Use ConstraintDescriptions(MyRequest.class).descriptionsForProperty("field") to read Bean Validation annotations at test time and inject them as a snippet attribute, rendered by a custom request-fields template. The docs are generated from the same annotations that enforce validation, so they can't diverge.
- What's the fundamental tradeoff of REST Docs versus an annotation-scanning doc generator?REST Docs couples docs to passing tests, guaranteeing accuracy but demanding a maintained test per documented interaction and breaking the build on drift. Annotation scanners are lower-effort and produce a machine-readable contract but can document intended, not actual, behavior. It's accuracy-and-effort vs convenience-and-contract.
saying these in an interview costs you the question
- Claiming REST Docs emits OpenAPI/a machine-readable contract out of the box
- Not realizing per-call preprocessors replace configured defaults, silently dropping global masking
- Duplicating preprocessor chains and descriptor lists instead of centralizing them
- Hand-typing validation rules that duplicate (and drift from) Bean Validation annotations