Slide 1

Slide 1 text

Documenting RESTful APIs with Spring REST Docs By Jenn Strater @codeJENNerator

Slide 2

Slide 2 text

@codeJENNerator Notes For Those Viewing These Slides Online • Bulleted text like this indicates the key points mentioned on a previous slide. They may not have been included in the official presentation. • If this view does not support links, the links will work in the pdf. In speakerdeck, you can click the ‘download pdf’ button at the right.

Slide 3

Slide 3 text

@codeJENNerator Outline • API Documentation Background • Approaches to Documentation • Considerations
 • Test-Driven Documentation • Spring REST Docs • Examples

Slide 4

Slide 4 text

@codeJENNerator Follow Along https://speakerdeck.com/jlstrater/test-driven-docs-warsaw-jug-2018 https://github.com/jlstrater/groovy-spring-boot-restdocs-example https://github.com/ratpack/example-books https://github.com/jlstrater/spring-restdocs-public-api-example

Slide 5

Slide 5 text

@codeJENNerator About Me

Slide 6

Slide 6 text

@codeJENNerator

Slide 7

Slide 7 text

@codeJENNerator About Me • Co-founder of Gr8Ladies and talk about women in the Groovy Community all over the world • Passionate about bring new people into the Groovy community through free introductory workshops called Gr8Workshops. • Senior Engineer at Zenjob as of June 2017. We’re hiring! zenjob.de/ careers • Spent the 2016-2017 academic year in Copenhagen working on OSS and taking classes through a Fulbright Grant. • Prior to the Fulbright Grant, I was a senior consultant at Object Partners, Inc. in Minneapolis, MN, USA. My work with Spring REST Docs started on a project at my client through them.

Slide 8

Slide 8 text

@codeJENNerator Audience Background • Creating RESTful APIs • Spring Boot • Grails • Ratpack • API Documentation • Wiki Pages, Word Documents, Confluence, etc • Asciidoc / Asciidoctor • Swagger / RAML

Slide 9

Slide 9 text

@codeJENNerator src: https://flic.kr/p/rehEf5

Slide 10

Slide 10 text

@codeJENNerator src: https://flic.kr/p/rehEf5 I hate writing documentation!*

Slide 11

Slide 11 text

@codeJENNerator Case Study

Slide 12

Slide 12 text

REST Maturity Model src: http://martinfowler.com/articles/richardsonMaturityModel.html

Slide 13

Slide 13 text

REST Maturity Model src: http://martinfowler.com/articles/richardsonMaturityModel.html

Slide 14

Slide 14 text

@codeJENNerator REST Maturity Model • I’m not really a fan of the right vs wrong REST debate, but I like this categorization of APIs. • Most of our APIS were level one or two, but we wanted to have the flexibility to use hypermedia • Spring REST docs includes support for level 3 / hypermedia • Swagger 2.0 did not support hypermedia. Swagger 3.0 (released end of July 2017) now does

Slide 15

Slide 15 text

Documenting RESTfullike APIs with Spring REST Docs By Jenn Strater @codeJENNerator

Slide 16

Slide 16 text

@codeJENNerator REST Maturity Model Attribution: @Alvaro_Sanchez

Slide 17

Slide 17 text

@codeJENNerator Monolith vs Microservices • As architecture evolves, many companies move from a central monolith to micro services or maybe even gateways and multi-tiered architectures. • For documentation, it was important to have: • a consistent look and feel • a way to show how the services can work together

Slide 18

Slide 18 text

@codeJENNerator Central Information Security Http Verbs Error Handling Http Status

Slide 19

Slide 19 text

@codeJENNerator Central Information • Central Information • For example, security tokens, patterns for error messages, http verbs/status codes, etc • This information needs to be written out and defined once; not on every endpoint.

Slide 20

Slide 20 text

@codeJENNerator Document Design

Slide 21

Slide 21 text

@codeJENNerator URI Centric

Slide 22

Slide 22 text

@codeJENNerator URI Centric

Slide 23

Slide 23 text

@codeJENNerator Central Information • Who’s seen this before? Of the people who have never used swagger before, how many understand what this means? Even our CTO who is technical, didn’t want to spend time figuring it out. Also, product teams. • This is a swagger ui example but the concept is not limited to Swagger. I have seen many different APIs document in this way. It’s not just URI centric, but also very developer centric. No matter whether you leave here choosing Swagger or Spring REST Docs, think about your users! • This is an example from Spring Rest Docs using Asciidoc. • Notice the very different way of organizing information on the second slide. Resource centric document design organizes information by topic and includes urls in the examples only. The information from generated solutions isn’t enough. We need the handwritten information too!

Slide 24

Slide 24 text

@codeJENNerator Available Tools

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

Definitions Swagger (OpenAPI Specification) RAML

Slide 27

Slide 27 text

Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis

Slide 28

Slide 28 text

Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup

Slide 29

Slide 29 text

Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup

Slide 30

Slide 30 text

Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup AssertJ- Swagger Contract-First

Slide 31

Slide 31 text

Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First

Slide 32

Slide 32 text

Definitions Swagger (OpenAPI Specification) RAML

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

The specification formerly known as

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

@codeJENNerator Swagger • Swagger is: — a lot of things • At the core, it is a way to standardize and define HTTP APIs over RPC. • It is very popular because of the many plugins built on top of it for things such as generating client libraries, generating docs, and much more. • In earlier versions, it did not support hypermedia. Documenting across micro services was possible, but required a bit of setup. Depending on the library, some central information is duplicated.

Slide 37

Slide 37 text

Body Slide - Dark Background All body text is Proxima Nova Regular • Subhead (18pt) • Level Two (18pt) • Level Three (18pt) • Level Four (18pt) Use the “Decrease/Increase Indent” 
 tools to change bullet levels Automation img src: https://flic.kr/p/eduUfU

Slide 38

Slide 38 text

Body Slide - Dark Background All body text is Proxima Nova Regular • Subhead (18pt) • Level Two (18pt) • Level Three (18pt) • Level Four (18pt) Use the “Decrease/Increase Indent” 
 tools to change bullet levels img src: https://www.flickr.com/photos/ 24874528@N04/17125924230 SpringFox

Slide 39

Slide 39 text

@codeJENNerator SpringFox • SpringFox: • Generates a Swagger Specification from source • Is very easy to setup (in simple cases) • No OpenAPI Spec 3.0 support!

Slide 40

Slide 40 text

@codeJENNerator Custom Swagger Specification { "swagger": "2.0", "info": { "version": "1", "title": "My Service", "contact": { "name": "Company Name" }, "license": {} }, "host": "example.com", "basepath": "/docs" }

Slide 41

Slide 41 text

@codeJENNerator Swagger UI

Slide 42

Slide 42 text

@codeJENNerator URI Centric

Slide 43

Slide 43 text

@codeJENNerator SpringFox UI approaches • Use SpringFox library • Copy static files and customize

Slide 44

Slide 44 text

@codeJENNerator Considerations

Slide 45

Slide 45 text

@codeJENNerator Customization img src: http://sergiodelamo.es/how-to-secure-your-grails-3-api-with-spring-security-rest-for-grails/

Slide 46

Slide 46 text

@codeJENNerator Customization • For any non-standard configuration, you may have to override the UI. • As one example, we were adding custom headers for oauth jwt tokens. At the time, it was not supported with springfox-ui.

Slide 47

Slide 47 text

@codeJENNerator Object Mapping img src: https://github.com/springfox/springfox/issues/281 img src: https://github.com/springfox/springfox/issues/281

Slide 48

Slide 48 text

@codeJENNerator img src: https://github.com/springfox/springfox/issues/281 https://github.com/swagger-api/swagger-core/issues/97

Slide 49

Slide 49 text

@codeJENNerator Customization • In Swagger/OpenAPI Spec 2.0, there was no hypermedia support. • In OpenAPI Spec 3.0, there is some support for links

Slide 50

Slide 50 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 }

Slide 51

Slide 51 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } Annotation Hell

Slide 52

Slide 52 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } Annotation Hell

Slide 53

Slide 53 text

@codeJENNerator 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/actionName', method = 3 RequestMethod.POST)
 4 @ApiOperation(value = '/actionName',
 5 notes = 'Enables or disables setting via "1" or "0", respectively')
 6 @ApiResponses(value = [
 7 @ApiResponse(code = 200, response = CustomSettingResponse, message = 8 ‘Successful setting update'),
 9 @ApiResponse(code = 400, response = ErrorResponse, message = 'Invalid 10 user input'),
 11 @ApiResponse(code = 500, response = ErrorResponse, message = 'Unexpected 12 server error')
 13 ])
 14 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 15 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 16 settingsValue.fieldOne,
 17 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 18 new Double(settingsValue.value))]
 19 )
 20 api.saveUpdatedSetting(request)
 21 } Annotation Hell X

Slide 54

Slide 54 text

@codeJENNerator Annotation Hell 1 @Secured(‘ROLE_ALLOWED_TO_PERFORM_ACTION’)
 2 @RequestMapping(value = '/v1/serviceName/ actionName', method = 3 RequestMethod.POST) 4 CustomSettingResponse setSetting(@RequestBody CustomModel settingsValue) {
 5 SaveSettingUpdateRequest request = new SaveSettingUpdateRequest (
 6 settingsValue.fieldOne,
 7 [new TransformedSetting(SettingEnum.POSSIBLE_ENUM_VALUE, 8 new Double(settingsValue.value))]
 9 )
 10 api.saveUpdatedSetting(request)
 11 }

Slide 55

Slide 55 text

@codeJENNerator Swagger Advantages

Slide 56

Slide 56 text

@codeJENNerator

Slide 57

Slide 57 text

@codeJENNerator “Try it” Button Alternatives

Slide 58

Slide 58 text

Curl -> curl 'http://localhost:8080/greetings' -i -H 'Content-Type: text/plain' HTTP/1.1 200 X-Application-Context: application:8080 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 26 Jan 2017 13:28:19 GMT [{"id":1,"message":"Hello"},{"id":2,"message":"Hi"},{"id":3,"message":"Hola"},{"id": 4,"message":"Olá"},{"id":5,"message":"Hej"}]

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

@codeJENNerator

Slide 61

Slide 61 text

@codeJENNerator https://www.npmjs.com/package/ restdocs-to-postman

Slide 62

Slide 62 text

@codeJENNerator Mix and Match

Slide 63

Slide 63 text

Swagger2Markup https://github.com/Swagger2Markup/swagger2markup

Slide 64

Slide 64 text

img src: http://www.elvenspirit.com/elf/wp-content/uploads/2011/10/IMG_3013.jpg FAIL!

Slide 65

Slide 65 text

img src: http://www.elvenspirit.com/elf/wp-content/uploads/2011/10/IMG_3013.jpg FAIL! AssertJ-Swagger https://github.com/RobWin/assertj-swagger

Slide 66

Slide 66 text

@codeJENNerator Spring Cloud Contract https://cloud.spring.io/spring-cloud-contract/

Slide 67

Slide 67 text

@codeJENNerator Stubs

Slide 68

Slide 68 text

@codeJENNerator Stubs

Slide 69

Slide 69 text

@codeJENNerator What problem are you trying to solve?

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

Definitions Swagger (OpenAPI Specification) RAML

Slide 72

Slide 72 text

Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis

Slide 73

Slide 73 text

Definitions Swagger (OpenAPI Specification) RAML Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup

Slide 74

Slide 74 text

Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup

Slide 75

Slide 75 text

Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup AssertJ- Swagger Contract-First

Slide 76

Slide 76 text

Definitions Swagger (OpenAPI Specification) RAML Testing MockMVC RestAssured Documentation AsciiDoc Markdown Wikis Swagger UI Swagger2Markup Spring REST Docs AssertJ- Swagger Contract-First

Slide 77

Slide 77 text

Spring REST Docs

Slide 78

Slide 78 text

@codeJENNerator Green Red Refactor Test-Driven Development

Slide 79

Slide 79 text

@codeJENNerator Test-Driven Documentation

Slide 80

Slide 80 text

@codeJENNerator Red Test-Driven Documentation

Slide 81

Slide 81 text

@codeJENNerator Red Test-Driven Documentation

Slide 82

Slide 82 text

@codeJENNerator Document Red Test-Driven Documentation

Slide 83

Slide 83 text

@codeJENNerator Document Red Test-Driven Documentation

Slide 84

Slide 84 text

@codeJENNerator Document Green Red Test-Driven Documentation

Slide 85

Slide 85 text

@codeJENNerator Document Green Red Test-Driven Documentation

Slide 86

Slide 86 text

@codeJENNerator Document Green Red Refactor Test-Driven Documentation

Slide 87

Slide 87 text

@codeJENNerator Document Green Red Refactor Test-Driven Documentation

Slide 88

Slide 88 text

Winning Solution Test-Driven Documentation https://flic.kr/p/5XiKxU Winning Solution

Slide 89

Slide 89 text

@codeJENNerator Winning Solution • Ensures documentation matches implementation • Encourages writing more tests • Reduces duplication in docs and tests • Removes annotations from source

Slide 90

Slide 90 text

@codeJENNerator Spring REST Docs https://flic.kr/p/5XiKxU

Slide 91

Slide 91 text

Game Changers https://flic.kr/p/9Tiv3U

Slide 92

Slide 92 text

@codeJENNerator Game Changers •Generated code snippets •Tests fail when documentation is missing or out-of- date •Rest APIs with Hypermedia •Ratpack •Dynamic routing doesn’t work with Swagger

Slide 93

Slide 93 text

@codeJENNerator About Spring REST Docs projects.spring.io/spring-restdocs @springrestdocs https://github.com/spring-projects/spring-restdocs

Slide 94

Slide 94 text

@codeJENNerator About Spring REST Docs projects.spring.io/spring-restdocs @springrestdocs https://github.com/spring-projects/spring-restdocs

Slide 95

Slide 95 text

@codeJENNerator About Spring REST Docs •Start with reading the docs; The written docs are good! •Overview •Sponsored by Pivotal •Project Lead - Andy Wilkinson •Current Version - 2.0.1 released April 4 •Twitter Account and Official Logo

Slide 96

Slide 96 text

@codeJENNerator About Spring REST Docs • Test-Driven Documentation with Spring REST Docs (Java and Spring Boot) - Spring I/O 2016 Andy Wilkinson • Writing comprehensive and guaranteed up-to-date REST API documentation - SpringOne Platform 2016 Anders Evers

Slide 97

Slide 97 text

@codeJENNerator Out of the Box

Slide 98

Slide 98 text

@codeJENNerator Out of the Box • Testing Frameworks • MockMVC • RestAssured • WebTestClient - NEW! • Build Tools • Gradle • Maven

Slide 99

Slide 99 text

@codeJENNerator Out of the Box

Slide 100

Slide 100 text

@codeJENNerator Out of the Box Documentation Format • AsciiDoc • Markdown Sample Projects • Spring Boot • Grails • Slate • TestNG • And more!

Slide 101

Slide 101 text

@codeJENNerator Examples

Slide 102

Slide 102 text

@codeJENNerator Groovier Spring REST Docs • Spring Boot • Ratpack • Grails

Slide 103

Slide 103 text

@codeJENNerator Groovier Spring REST Docs Example - Spring Boot • Groovy Spring Boot Project • + Asciidoctor Gradle plugin • + Spring REST Docs WebTestClient to Spock tests • + Add to static assets during build and publish to GitHub pages

Slide 104

Slide 104 text

@codeJENNerator Groovier Spring REST Docs

Slide 105

Slide 105 text

@codeJENNerator Groovier Spring REST Docs

Slide 106

Slide 106 text

@codeJENNerator Groovier Spring REST Docs

Slide 107

Slide 107 text

@codeJENNerator Groovier Spring REST Docs Example - Spring Boot • Start with lazybones spring boot app • Add mock endpoints for example https://github.com/jlstrater/groovy-spring-boot- restdocs-example • Updated to Spring Boot 2.0.0.M7 https://www.callicoder.com/reactive-rest-apis-spring- webflux-reactive-mongo/

Slide 108

Slide 108 text

@codeJENNerator

Slide 109

Slide 109 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } }

Slide 110

Slide 110 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting

Slide 111

Slide 111 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings

Slide 112

Slide 112 text

@codeJENNerator @RestController @RequestMapping('/greetings') class GreetingsController { @Autowired GreetingRepository greetingsRepository @PostMapping() Mono createGreeting(@Valid @RequestBody Greeting greeting) { return greetingsRepository.save(greeting) } @GetMapping() Flux listAllGreetings() { return greetingsRepository.findAll() } @GetMapping('/{id}') Mono getGreetingById(@PathVariable(value = 'id') String greetingId) { greetingsRepository.findById(greetingId) } } Create a new Greeting List all greetings Get a greeting by id

Slide 113

Slide 113 text

@codeJENNerator

Slide 114

Slide 114 text

@codeJENNerator

Slide 115

Slide 115 text

@codeJENNerator AsciiDoc • [introduction] • = Introduction • The Example API is a RESTful web service that shows how Spring REST docs works. • [[overview-http-verbs]] • == HTTP verbs • The Example API tries to adhere as closely as possible to standard HTTP and REST conventions in its • use of HTTP verbs. • |=== • | Verb | Usage • | `GET` • | Used to retrieve a resource

Slide 116

Slide 116 text

@codeJENNerator Asciidoctor Gradle Plugin

Slide 117

Slide 117 text

@codeJENNerator AsciiDoc Gradle Configuration apply plugin: 'org.asciidoctor.convert' asciidoctor { backends 'html5' attributes 'source-highlighter' : 'prettify', 'imagesdir':'images', 'toc':'left', 'icons': 'font', 'setanchors':'true', 'idprefix':'', 'idseparator':'-', 'docinfo1':'true', }

Slide 118

Slide 118 text

@codeJENNerator

Slide 119

Slide 119 text

@codeJENNerator

Slide 120

Slide 120 text

@codeJENNerator

Slide 121

Slide 121 text

@codeJENNerator

Slide 122

Slide 122 text

@codeJENNerator

Slide 123

Slide 123 text

Project Reactor and the WebTestClient

Slide 124

Slide 124 text

@codeJENNerator Setup @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .build() } }

Slide 125

Slide 125 text

@codeJENNerator Setup @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .build() } } If context is null, remember to use spock-spring!!

Slide 126

Slide 126 text

@codeJENNerator WebTestClient Call and Assertions this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": “Hello Warsaw JUG!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath(‘$.message').isEqualTo('Hello Warsaw JUG!’)

Slide 127

Slide 127 text

@codeJENNerator

Slide 128

Slide 128 text

@codeJENNerator

Slide 129

Slide 129 text

@codeJENNerator Spring REST Docs Gradle Configuration dependencies { … testCompile "org.springframework.restdocs:spring-restdocs-webtestclient:${springRestDocsVersion}" asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${springRestDocsVersion}" } ext { snippetsDir = file('build/generated-snippets') } test { outputs.dir "$projectDir/src/main/resources/public" } asciidoctor { dependsOn test inputs.dir snippetsDir }

Slide 130

Slide 130 text

@codeJENNerator Spring REST Docs with WebTestClient (setup) @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation)) .build() }

Slide 131

Slide 131 text

@codeJENNerator Spring REST Docs with WebTestClient (setup) @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation)) .build() }

Slide 132

Slide 132 text

@codeJENNerator Spring REST Docs with WebTestClient (setup) @CompileStatic @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class BaseControllerSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Autowired ApplicationContext context protected WebTestClient webTestClient void setup() { this.webTestClient = WebTestClient.bindToApplicationContext(this.context) .configureClient() .baseUrl('/greetings') .filter(documentationConfiguration(restDocumentation)) .build() }

Slide 133

Slide 133 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document creating a greeting with a custom name'() { expect: this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello Warsaw JUG!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello Warsaw JUG!') .consumeWith(document('greetings-post-example', preprocessRequest(prettyPrint()), requestFields( fieldWithPath('message').type(JsonFieldType.STRING) .description("The greeting's message"))))

Slide 134

Slide 134 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document creating a greeting with a custom name'() { expect: this.webTestClient.post().uri('/') .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject('{"message": "Hello Warsaw JUG!"}')) .exchange() .expectStatus().isOk() .expectBody() .jsonPath('$.id').isNotEmpty() .jsonPath('$.message').isEqualTo('Hello Warsaw JUG!') .consumeWith(document('greetings-post-example', preprocessRequest(prettyPrint()), requestFields( fieldWithPath('message').type(JsonFieldType.STRING) .description("The greeting's message"))))

Slide 135

Slide 135 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document get of a list of greetings'() { expect: this.webTestClient.get().uri('/').accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document('greetings-list-example', preprocessResponse(prettyPrint()), responseFields(greetingList))) } FieldDescriptor[] greetingList = new FieldDescriptor().with { [fieldWithPath('[].id').type(JsonFieldType.STRING).optional() .description("The greeting's id"), fieldWithPath('[].message').type(JsonFieldType.STRING) .description("The greeting's message")]

Slide 136

Slide 136 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document get of a list of greetings'() { expect: this.webTestClient.get().uri('/').accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document('greetings-list-example', preprocessResponse(prettyPrint()), responseFields(greetingList))) } FieldDescriptor[] greetingList = new FieldDescriptor().with { [fieldWithPath('[].id').type(JsonFieldType.STRING).optional() .description("The greeting's id"), fieldWithPath('[].message').type(JsonFieldType.STRING) .description("The greeting's message")]

Slide 137

Slide 137 text

@codeJENNerator Spring REST Docs with WebTestClient (tests) void 'test and document get of a list of greetings'() { expect: this.webTestClient.get().uri('/').accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document('greetings-list-example', preprocessResponse(prettyPrint()), responseFields(greetingList))) } FieldDescriptor[] greetingList = new FieldDescriptor().with { [fieldWithPath('[].id').type(JsonFieldType.STRING).optional() .description("The greeting's id"), fieldWithPath('[].message').type(JsonFieldType.STRING) .description("The greeting's message")]

Slide 138

Slide 138 text

Error Messages

Slide 139

Slide 139 text

Error Messages

Slide 140

Slide 140 text

Error Messages

Slide 141

Slide 141 text

Error Messages

Slide 142

Slide 142 text

Error Messages

Slide 143

Slide 143 text

@codeJENNerator

Slide 144

Slide 144 text

@codeJENNerator

Slide 145

Slide 145 text

Generated Snippets By Default When Specified curl-request.adoc response-fields.adoc http-request.adoc request-parameters.adoc httpie-request.adoc request-parts.adoc http-response.adoc path-parameters.adoc request body request-parts.adoc response body

Slide 146

Slide 146 text

@codeJENNerator http-request.adoc [source,http,options="nowrap"] ---- POST /greetings/ HTTP/1.1 Content-Type: application/json Accept: application/json Host: localhost:8080 Content-Length: 45 { "message" : "Hello Warsaw JUG!” } ----

Slide 147

Slide 147 text

@codeJENNerator response-fields.adoc |=== |Path|Type|Description |`id` |`String` |The greeting's id |`message` |`String` |The greeting's message |===

Slide 148

Slide 148 text

response-fields.adoc

Slide 149

Slide 149 text

@codeJENNerator +

Slide 150

Slide 150 text

@codeJENNerator +

Slide 151

Slide 151 text

@codeJENNerator Adding The Snippets [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]

Slide 152

Slide 152 text

@codeJENNerator Adding The Snippets [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]

Slide 153

Slide 153 text

@codeJENNerator Adding The Snippets [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]

Slide 154

Slide 154 text

@codeJENNerator Adding The Snippets [[overview-errors]] == Errors Whenever an error response (status code >= 400) is returned, the body will contain a JSON object that describes the problem. The error object has the following structure: include::{snippets}/error-example/response-fields.adoc[] For example, a request that attempts to delete on the greetings endpoint will produce a `405 Method Not Allowed` response: operation::error-example[snippets='curl-request,http-request,http-response'] [[resources]] = Resources include::resources/greetings.adoc[]

Slide 155

Slide 155 text

@codeJENNerator

Slide 156

Slide 156 text

@codeJENNerator

Slide 157

Slide 157 text

@codeJENNerator Building the docs • . src/docs/ asciidoc index.adoc build/asciidoc/ html5 index.html .gradlew asciidoctor

Slide 158

Slide 158 text

@codeJENNerator Building the docs • .

Slide 159

Slide 159 text

@codeJENNerator Publishing Strategies

Slide 160

Slide 160 text

@codeJENNerator Publishing Strategies • Hook in asciidoctor with the gradle build task • Run the asciidoctor test separately (but make sure to run AFTER the tests) • Send to static resources directory in the current app or send to a remote site (for example Github Pages

Slide 161

Slide 161 text

@codeJENNerator

Slide 162

Slide 162 text

@codeJENNerator http://api.example.com/docs

Slide 163

Slide 163 text

@codeJENNerator publish.gradle buildscript { repositories { jcenter() } dependencies { classpath 'org.ajoberstar:gradle-git:1.1.0' } } apply plugin: 'org.ajoberstar.github-pages' githubPages { repoUri = '[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git' pages { from(file('build/asciidoc/html5')) } }

Slide 164

Slide 164 text

@codeJENNerator publish.gradle buildscript { repositories { jcenter() } dependencies { classpath 'org.ajoberstar:gradle-git:1.1.0' } } apply plugin: 'org.ajoberstar.github-pages' githubPages { repoUri = '[email protected]:jlstrater/groovy-spring-boot-restdocs-example.git' pages { from(file('build/asciidoc/html5')) } } If you use this method, remember to deploy docs at the same time as the project!

Slide 165

Slide 165 text

@codeJENNerator http://jlstrater.github.io/groovy- spring-boot-restdocs-example

Slide 166

Slide 166 text

@codeJENNerator http://jlstrater.github.io/groovy- spring-boot-restdocs-example ./gradlew publish

Slide 167

Slide 167 text

@codeJENNerator ➜ build git:(master) restdocs-to-postman --input generated-snippets --export-format postman --determine-folder secondLastFolder --output postman-collection.json

Slide 168

Slide 168 text

@codeJENNerator Special Use Case

Slide 169

Slide 169 text

@codeJENNerator 
 +@WebMvcTest(controllers = GreetingsController)
 +@AutoConfigureRestDocs(
 + outputDir = "build/generated-snippets",
 + uriHost = “api.example.com”,
 + uriPort = 8080
 ) class BaseControllerSpec extends Specification { 
 // @Rule
 // JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/ docs/generated-snippets')
 
 + @Autowired
 protected MockMvc mockMvc
 //
 // @Autowired
 // private WebApplicationContext context
 //
 // void setup() {
 // this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
 // .apply(documentationConfiguration(this.restDocumentation))
 // .build()
 // } }

Slide 170

Slide 170 text

@codeJENNerator Groovier Spring REST Docs • Spring Boot • Ratpack • Grails

Slide 171

Slide 171 text

@codeJENNerator Support for Rest- Assured

Slide 172

Slide 172 text

@codeJENNerator

Slide 173

Slide 173 text

@codeJENNerator

Slide 174

Slide 174 text

@codeJENNerator

Slide 175

Slide 175 text

@codeJENNerator Groovier Spring REST Docs Example - Ratpack • Ratpack Example Project • https://github.com/ratpack/example-books • Spring RESTdocs RestAssured • https://github.com/ratpack/example-books/pull/25

Slide 176

Slide 176 text

@codeJENNerator Groovier Spring REST Docs Example - Ratpack path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } }

Slide 177

Slide 177 text

@codeJENNerator Groovier Spring REST Docs Example - Ratpack path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Get a book by ISBN /api/books/1932394842

Slide 178

Slide 178 text

@codeJENNerator Groovier Spring REST Docs Example - Ratpack path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Get a book by ISBN /api/books/1932394842 Get all books /api/books

Slide 179

Slide 179 text

@codeJENNerator Groovier Spring REST Docs Example - Ratpack path(":isbn") {
 def isbn = pathTokens["isbn"]
 
 byMethod {
 get {
 bookService.find(isbn).
 single().
 subscribe { Book book ->
 if (book == null) {
 clientError 404
 } else {
 render book
 }
 }
 } ... }
 } 
 byMethod {
 get {
 bookService.all().
 toList().
 subscribe { List books ->
 render json(books)
 }
 }
 post {
 parse(jsonNode()).
 observe().
 flatMap { input ->
 bookService.insert(
 input.get("isbn").asText(),
 input.get("quantity").asLong(),
 input.get("price").asDouble()
 )
 }.
 single().
 flatMap {
 bookService.find(it)
 }.
 single().
 subscribe { Book createdBook ->
 render createdBook
 }
 } } Get a book by ISBN /api/books/1932394842 Get all books /api/books Post to create a new book /api/books

Slide 180

Slide 180 text

https://github.com/jayway/rest-assured

Slide 181

Slide 181 text

@codeJENNerator abstract class BaseDocumentationSpec extends Specification {
 
 @Shared
 ApplicationUnderTest aut = new ExampleBooksApplicationUnderTest()
 
 @Rule
 JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation()
 
 protected RequestSpecification documentationSpec
 
 void setup() {
 this.documentationSpec = new RequestSpecBuilder()
 .addFilter(documentationConfiguration(restDocumentation))
 .build()
 }
 }

Slide 182

Slide 182 text

@codeJENNerator

Slide 183

Slide 183 text

@codeJENNerator Documenting Public APIs

Slide 184

Slide 184 text

@codeJENNerator Blog Post https://jennstrater.blogspot.com/2017/01/using-spring- rest-docs-to-document.html

Slide 185

Slide 185 text

@codeJENNerator Outcomes

Slide 186

Slide 186 text

@codeJENNerator Outcomes • Made it to production! :) • Team was happy with Spring REST Docs • Other dev teams like to see the examples

Slide 187

Slide 187 text

@codeJENNerator Read the Docs for More On… • Adding Security and Headers • Documenting Constraints • Hypermedia Support • XML Support • Using Markdown instead of Asciidoc • Third Party Extensions for WireMock(Spring Cloud Contracts), Jersey, AutoRestDocs, RAML

Slide 188

Slide 188 text

@codeJENNerator Conclusion

Slide 189

Slide 189 text

@codeJENNerator Conclusion • API documentation is complex • Choosing the right tool for the job not just about the easiest one to setup • Spring REST Docs is a promising tool to enforce good testing and documentation practices without muddying source code. • I still hate writing boilerplate documentation, but at least it’s a little less painful now.

Slide 190

Slide 190 text

@codeJENNerator Next Steps • Join #spring-restdocs on gitter • Join the Groovy Community on Slack - groovycommunity.com