Home | Source for Kata

Spring REST Docs

Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test.

How RESTful is your API

Comparison with Swagger

  1. Swagger doesn’t support hypermedia (Level 3 maturity model)

  2. Swagger documentation is done through several annotations which are added in your implementation classes

  3. API Implementation code overtime, gets overwhelmed with swagger annotations

  4. Documentation is URI centric

  5. Documentation and API could be out-of-sync overtime as there are no validations in place to verify correctness of API

Spring REST Docs Advantages

  1. API descriptions and explanation are done in separate .adoc files

  2. Ability to structure your documentation based on domain model rather than by URI’s

  3. asciidoctor code snippets are generated using Spring Mvc Test, which in turn are plugged into adoc files

  4. Guarantees that API documentation and implementation code are always in-sync.

  5. Any updates to request, response payloads will fail Unit Tests, until documentation is updated to reflect the changes. Forces you to update documentation for any implementation changes.

  6. Ability to document with different payloads and explain different test scenarios

Spring REST Docs Implementation

Add Dependency JARs

Add the following code to the build script:

build.gradle
testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:1.1.0.BUILD-SNAPSHOT"

Spring REST Docs configuration

ApiDocumentation.java
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); (1)
private RestDocumentationResultHandler document; (2)
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;

@Before
public void setUp() {
  this.document = document("{method-name}/",
      preprocessRequest(prettyPrint()),
      preprocessResponse(prettyPrint())); (3)

  this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
      .apply(documentationConfiguration(this.restDocumentation)) (4)
      .alwaysDo(this.document)
      .build();
}

@Test
public void getAccountsSimpleDoc() throws Exception {
  this.mockMvc.perform(get("/accounts"))
      .andExpect(status().isOk())
      .andDo(this.document); (5)
}
1 Hook for linking JUnit and Spring REST Docs for generating snippets
2 REST Docs snippet Result Handler for formatting result to tables
3 Configuration for RestDocumentationResultHandler
4 Hook for linking Spring Mvc Test to Spring REST Docs
5 Generates asciidoc snippets for curl-request, http-request, http-response, response-fields adoc files

asciidoctor Configuration

build.gradle
buildscript {
  repositories {
    mavenCentral()
    jcenter() (1)
        }
        dependencies {
                classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE")
    classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3' (2)
        }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'org.asciidoctor.convert' (3)

repositories {
        mavenLocal()
        maven { url 'https://repo.spring.io/libs-snapshot' }
        mavenCentral()
}
1 Repository for asciidoctor plugin
2 Add asciidoctor plugin to classpath
3 Apply asciidoctor plugin

Integrate Spring REST Docs with asciidoc

build.gradle
ext { (1)
  snippetsDir = file('build/generated-snippets')
}

test { (2)
        outputs.dir snippetsDir
}

asciidoctor { (3)
        sourceDir '../../../guides/src/kata-spring-restdocs'
        attributes 'snippets': snippetsDir
        inputs.dir snippetsDir
        dependsOn test
}
1 Define where snippets would be generated
2 Teach test task about snippets directory for gradle to know that when tests are run, snippets are generated in this directory
3 Tell asciidoctor about the snippets directory for generating live documentation in the process of gradle continuous builds

Document APIs using Spring REST Docs

ApiDocumentation.java
  @Test
  public void getIndexExample() throws Exception {
    this.document.snippets(
        links( (1)
            linkWithRel("accounts").description("The <<resources-accounts,Account Resource>>"),
            linkWithRel("contacts").description("The <<resources-contacts,Contact Resource>>")),
        responseFields( (2)
            fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources")));

    this.mockMvc.perform(get("/"))
        .andExpect(status().isOk());
  }

  @Test
  public void getAccountsExample() throws Exception {
    this.document.snippets(
        responseFields(
            fieldWithPath("_links.self").description("Resource Self Link"),
            fieldWithPath("_embedded.accountList").description("An array of <<resources-account,Account resources>>")));

    this.mockMvc.perform(get("/accounts"))
        .andExpect(status().isOk());
  }

  @Test
  public void getAccountExample() throws Exception {
    this.document.snippets(
        responseFields(
            fieldWithPath("id").description("Account unique identifier"),
            fieldWithPath("name").description("Account name"),
            fieldWithPath("_links.self").description("Account Resource Self Link"),
            fieldWithPath("_links.account-contacts").description("Contacts associated for given Account")));

    this.mockMvc.perform(get("/accounts/1"))
        .andExpect(status().isOk());
//        .andExpect(jsonPath("id", is(notNullValue())))
//        .andExpect(jsonPath("name", is(notNullValue())))
//        .andExpect(jsonPath("_links.self", is(notNullValue())))
//        .andExpect(jsonPath("_links.account-contacts", is(notNullValue())));
  }
1 Documenting Link Relationship
2 Documenting Response fields

Generate documentation

Running test target on the project generates the documentation:

$ gradlew test

The results of the run will generate documentation that can now be integrated and presented in a cohesive way. It may look like the one below …​

Service API Guide

Overview

A sample kata service with resources - Accounts and Contact.
Accounts can have many Contacts. A given contact can belong to many Accounts

Index

The index provides the entry point into the service.

Accessing the index

A GET request is used to access the index

Example request
$ curl 'http://localhost:8080/' -i
Response structure
Path Type Description

_links

Object

Links to other resources

Example response
HTTP/1.1 200 OK
Content-Type: application/hal+json
Content-Length: 178

{
  "_links" : {
    "accounts" : {
      "href" : "http://localhost:8080/accounts"
    },
    "contacts" : {
      "href" : "http://localhost:8080/contacts"
    }
  }
}
Relation Description

accounts

The Account Resource

contacts

The Contact Resource

Account

The Account resources

Listing Accounts

A GET request will list all of the service’s accounts.

Response structure
Path Type Description

_links.self

Object

Resource Self Link

_embedded.accountList

Array

An array of Account resources

Example request
$ curl 'http://localhost:8080/accounts' -i
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 967

{
  "_embedded" : {
    "accountList" : [ {
      "id" : 1,
      "name" : "John",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/accounts/1"
        },
        "account-contacts" : {
          "href" : "http://localhost:8080/accounts/1/contacts"
        }
      }
    }, {
      "id" : 2,
      "name" : "Tim",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/accounts/2"
        },
        "account-contacts" : {
          "href" : "http://localhost:8080/accounts/2/contacts"
        }
      }
    }, {
      "id" : 3,
      "name" : "Mike",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/accounts/3"
        },
        "account-contacts" : {
          "href" : "http://localhost:8080/accounts/3/contacts"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/accounts"
    }
  }
}

Retrieve a Account

A GET request will retrieve the details of a Account

Response structure
Path Type Description

id

Number

Account unique identifier

name

String

Account name

_links.self

Object

Account Resource Self Link

_links.account-contacts

Object

Contacts associated for given Account

Example request
$ curl 'http://localhost:8080/accounts/1' -i
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 228

{
  "id" : 1,
  "name" : "John",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/accounts/1"
    },
    "account-contacts" : {
      "href" : "http://localhost:8080/accounts/1/contacts"
    }
  }
}

Contact

The Contact resource is used to provide contact information

Retrieve a Contact

A GET request will retrieve the details of a contact

Response structure
Path Type Description

_embedded.contactList

Array

An array of Contact resources

Example request
$ curl 'http://localhost:8080/accounts/1/contacts' -i
Example response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 619

{
  "_embedded" : {
    "contactList" : [ {
      "id" : 1,
      "address" : "Waltham, MA",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/contacts/1"
        },
        "contact-accounts" : {
          "href" : "http://localhost:8080/contacts/1/accounts"
        }
      }
    }, {
      "id" : 2,
      "address" : "Boston, MA",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/contacts/2"
        },
        "contact-accounts" : {
          "href" : "http://localhost:8080/contacts/2/accounts"
        }
      }
    } ]
  }
}

References