Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Leverage HTTP Status Codes to Build a REST Service

DZone's Guide to

Leverage HTTP Status Codes to Build a REST Service

You can learn how to build a RESTful web service in Java with appropriate HTTP code responses in this step by step tutorial.

Free Resource

The Integration Zone is brought to you in partnership with Cloud Elements.  What’s below the surface of an API integration? Download The Definitive Guide to API Integrations to start building an API strategy.

With the emergence of microservice architectures and single-page applications, web services are everywhere. It’s straightforward to build a REST backend service, more difficult to write a good service which returns appropriate responses. In this article, which is also a tutorial, we will see how to write a simple CRUD REST Spring Boot service based on well-used HTTP status and fully covered by tests.

Requirements

  • JDK 8

  • Maven 3.x

Project Initialization

To bootstrap a Spring Boot application, we could start the project from scratch with our favorite IDE, or just use another way which makes life easier: Spring Initializr.

Opening https://start.spring.io leads us to a web page where we can select a build system- Maven or Gradle- and all dependencies we want to use for our project. In my case, I selected Maven with Web, JPA, H2, and HATEOAS dependencies on Spring Boot 2.0.0 M4 platform:

Spring initializr

Notice that some IDE integrates directly Spring Initializr like Eclipse Spring Tool Suite for instance. Then, we click on Generate Project button to download the Spring Boot starter project. At this time, we just need to import the unzipped project into our IDE (IntelliJ in my case). The image below shows the final project structure:

Image title

I also added one extra dependency in the pom.xml file:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.6</version>
</dependency>

Apache Commons Lang provides ToStringBuilder, which helps us to easily generate toString method.

Books Web Service

The aim of this article is to build a CRUD service, so I choose to build a simple books service. Like I said in the introduction, it's straightforward to build a service. The easiest way is to return a 200 HTTP status code when all is OK, and otherwise, a 404 status code. But, if I have a 404 response when I retrieve a resource, does that means the service is unreachable, there is no response, or I provided bad request data?

That's why there are many HTTP statuses you must use to help the client make a good decision if the service returns an error, but also a valid response.

HTTP Status Codes

HTTP status codes are divided into five categories:

  1. Informational 1xx: indicates that the request was received and understood.

  2. Successful 2xx: indicates the action requested by the client was received, understood, accepted, and processed successfully.

  3. Redirection 3xx: indicates the client must take additional action to complete the request.

  4. Client Error 4xx: indicates the client seems to have erred.

  5. Server Error 5xx: indicates the server has encountered an error during the process.

We will see these codes in this article:

200 Success The request has succeeded
201 Created The request has been fulfilled and resulted in a new resource being created
204 No Content The request has fulfilled the request but does not need to return an entity body
206 Partial Content The server has fulfilled the partial GET request for the resource
400 Bad request The request could not be understood by the server due to malformed syntax
404 Not Found The server has not found anything matching the request URI
405 Method Not allowed The method specified in the request is not allowed for the resource identified by the request URI
409 Conflict The request could not be completed due to a conflict with the current state of resource

For further information about other status codes, you can follow this link: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.

API Endpoints

Here are API endpoints I want:

HTTP method URI Description Valid HTTP status codes
POST /api/books Create a book 201
GET /api/books/{isbn} Read a book 200
PUT /api/books/{isbn} Update a book 200
DELETE /api/books/{isbn} Delete a book 204
PATCH /api/books/{isbn} Update book description 200
GET /api/books Retrieve all books by pagination, sorting and ordering 200, 204, 206

You'll notice that the four first lines are CRUD operations: Create, Read, Update, Delete. We also have two others operations: one to update book's description which uses a PATCH verb and one to retrieve all books.

Building the Service

Book Domain Class

We first need to write our Book class:

@Entity
@Table(uniqueConstraints = { @UniqueConstraint(name = "uk_book_isbn", columnNames = "isbn") })
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String isbn;

    @NotBlank
    private String title;

    private String description;

    @ElementCollection
    @NotEmpty
    private Set<Author> authors;

    @NotBlank
    private String publisher;

    // ----------------
    // - CONSTRUCTORS -
    // ----------------

    private Book() {
        // Default constructor for Jackson
    }

    public Book(String isbn, String title, Set<Author> authors, String publisher) {
        this.isbn = isbn;
        this.title = title;
        this.authors = authors;
        this.publisher = publisher;
    }

    public Book(String isbn, String title, String publisher) {
        this(isbn, title, new HashSet<>(), publisher);
    }

    // -----------
    // - METHODS -
    // -----------

    public void addAuthor(Author author) {
        this.authors.add(author);
    }

    // -------------
    // - TO STRING -
    // -------------

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
                .append("id", id)
                .append("isbn", isbn)
                .append("title", title)
                .append("description", description)
                .append("authors", authors)
                .append("publisher", publisher)
                .toString();
    }

...
}

A book is an entity in terms of JPA. A book is identified by its technical id, but also its international standard book number (ISBN) code, which is unique. A book has a title and a description (which is optional in our case), one or several authors (for simplicity, I made an embedded collection instead of a one-to-many) and a publisher.

I didn't mention all setters/getters you'll ever know. If you don't like to write boring setters/getters, you can use the Lombok project, which just makes the stuff for you.

A embeddable author looks like this:

@Embeddable
public class Author {

    private String firstName;
    private String lastName;

    private Author() {
        // Default constructor for Jackson
    }

    public Author(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
                .append("firstName", firstName)
                .append("lastName", lastName)
                .toString();
    }
}

Nothing else to say, an author is a person with firstName and lastName.

Book Repository and Database Data

As we are dealing with a book collection, we need to store them in a database. As it's not a production service, I choose H2 database in-memory, which fits well for our use case:

# H2
spring.h2.console.enabled=true
spring.h2.console.path=/h2

# Datasource
spring.datasource.url=jdbc:h2:mem:book
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver

Enabling H2 console provides us with a small web interface where we can query data without the need to use a specific client:

Image title

Book Repository

We need a repository to interact with the book entity. We use Spring Data JPA to do the work for us. Extending CrudRepository provides us with CRUD methods, but we also need pagination and sorting methods, so we can extend our repository to PagingAndSortingRepository, which inherits from CrudRepository:

public interface BookRepository extends PagingAndSortingRepository<Book, Long> {    
  Optional<Book> findByIsbn(String isbn);
}

Instead of retrieving a book by its technical id, it's better to find a book by its ISBN. I also prefer to return an Optional to indicate you don't necessarily have a book.

Dataset

We can import some test data by including import.sql into the resource folder:

-- books
insert into book(isbn,title,publisher) values ('978-0321356680','Effective Java','Addison Wesley');
insert into book(isbn,title,publisher) values ('978-1617292545','Spring Boot in Action','Manning Publications');
insert into book(isbn,title,publisher) values ('978-1491900864','Java 8 Pocket Guide','O''Reilly');
insert into book(isbn,title,publisher) values ('978-0321349606','Java Concurrency in Practice','Addison Wesley');

-- authors
insert into book_authors(book_id,first_name,last_name) values (1,'Joshua', 'Blosh');
insert into book_authors(book_id,first_name,last_name) values (2,'Craig', 'Walls');
insert into book_authors(book_id,first_name,last_name) values (3,'Robert', 'Liguori');
insert into book_authors(book_id,first_name,last_name) values (3,'Patricia', 'Liguori');
insert into book_authors(book_id,first_name,last_name) values (4,'Brian', 'Goetz');
insert into book_authors(book_id,first_name,last_name) values (4,'Joshua', 'Blosh');
insert into book_authors(book_id,first_name,last_name) values (4,'Joseph', 'Bowbeer');
insert into book_authors(book_id,first_name,last_name) values (4,'Tim', 'Peierls');

Book Controller

Here we are, the backbone of our book API:

@RestController
@RequestMapping(value = "/api/books")
public class BookController {

    private final BookRepository bookRepository;

    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

There is nothing at this time, but it will grow by adding each API method. We just inject our BookRepository into the BookController. As it's a tutorial, I omitted a service layer; I don't need to make transactional business calls.

Create a Book

Creating a book needs first to check a book is not already present in the database with the same ISBN. If not, we can save the book and send an empty response with the 201 status, else we need to inform the client a book is present:

@PostMapping
public ResponseEntity<?> createBook(@Valid @RequestBody Book book, UriComponentsBuilder ucBuilder) {
    if (bookRepository.findByIsbn(book.getIsbn()).isPresent()) {
        throw new BookIsbnAlreadyExistsException(book.getIsbn());
    }
    bookRepository.save(book);

    HttpHeaders headers = new HttpHeaders();
    headers.setLocation(ucBuilder.path("/api/books/{isbn}").buildAndExpand(book.getIsbn()).toUri());
    return new ResponseEntity<>(headers, HttpStatus.CREATED);
}

You could also catch the unique constraint violation and send the exception instead of verifying the book's presence. We need to send where the new book resource can be located, so the client is able to parse the header response and make a new request to retrieve the book's data.

Spring provides us an easy way to test our controller through MockMvc. We just need to create a controller test class specifying AutoConfigureMockMvc annotation to auto-configure it:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = JavaRestBooksApplication.class)
@AutoConfigureMockMvc
@Transactional
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    private HttpMessageConverter mappingJackson2HttpMessageConverter;

    @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {
        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream()
                .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
        Assert.assertNotNull("the JSON message converter must not be null", this.mappingJackson2HttpMessageConverter);
    }

    @SuppressWarnings("unchecked")
    protected String json(Object o) throws IOException {
        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
        this.mappingJackson2HttpMessageConverter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
        return mockHttpOutputMessage.getBodyAsString();
    }
}
  ...

Don't forget to set the Transactional annotation to isolate each test, otherwise you will have failed tests by running them all together. I made a json helper method to marshal Java object into JSON.

We first test a valid book creation and verify the presence of the header location and the correct returned status code:

@Test
public void should_create_valid_book_and_return_created_status() throws Exception {
    Book book = new Book("123-1234567890","My new book","Publisher");
    book.addAuthor(new Author("John","Doe"));

    mockMvc.perform(post("/api/books")
        .contentType(MediaType.APPLICATION_JSON)
        .content(json(book)))
        .andExpect(status().isCreated())
        .andExpect(header().string("Location", is("http://localhost/api/books/123-1234567890")))
        .andExpect(content().string(""))
        .andDo(MockMvcResultHandlers.print());
}

In each test, I put MockMvcResultHandlers.print() to display the result. You don't need this in a production environment, it's just useful if you want to print easily results when you debug, for instance.

The next test shows the bad request use-case which must send a 400 response:

@Test
public void should_not_create_invalid_content_book_and_return_bad_request_status() throws Exception {
    Book book = new Book(null,"My new book","Publisher");

    mockMvc.perform(post("/api/books")
        .contentType(MediaType.APPLICATION_JSON)
        .content(json(book)))
        .andExpect(status().isBadRequest())
        .andDo(MockMvcResultHandlers.print());
}

We can make another test, which is useless for test coverage but demonstrates how you can't call endpoints which don't allow other HTTP methods. In our case, only the POST method is allowed to create a book:

@Test
public void should_not_allow_others_http_methods() throws Exception {
    Book book = new Book("123-1234567890","My new book","Publisher");
    book.addAuthor(new Author("John","Doe"));

    mockMvc.perform(put("/api/books")
        .contentType(MediaType.APPLICATION_JSON)
        .content(json(book)))
        .andExpect(status().isMethodNotAllowed())
        .andExpect(content().string(""))
        .andDo(MockMvcResultHandlers.print());
}

And finally, we must also cover the already existing book use-case:

@Test
public void should_not_create_existing_book_and_return_conflict_status() throws Exception {
    Book book = new Book("978-0321356680","My new book","Publisher");
    book.addAuthor(new Author("John","Doe"));

    mockMvc.perform(post("/api/books")
        .contentType(MediaType.APPLICATION_JSON)
        .content(json(book)))
        .andExpect(status().isConflict())
        .andDo(MockMvcResultHandlers.print());
}

"But wait," you could say me, "I don't understand, you throw an exception in your code and it's a 409 status code?" You can map a specific status code to an exception by creating a ControllerAdvice and wrap them to produce a vnd.error:

@ControllerAdvice
@RequestMapping(produces = "application/vnd.error")
public class BookControllerAdvice {

    @ResponseBody
    @ExceptionHandler(BookIsbnAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    VndErrors bookIsbnAlreadyExistsExceptionHandler(BookIsbnAlreadyExistsException ex) {
        return new VndErrors("error", ex.getMessage());
    }
}

Read a Book

Getting a book is very easy. You return it with 200 status or send an exception which will be wrapped with the proper 404 status code by the BookControllerAdvice :

@GetMapping("/{isbn}")
public ResponseEntity<Book> getBook(@PathVariable("isbn") String isbn) {
    return bookRepository.findByIsbn(isbn)
        .map(book -> new ResponseEntity<>(book, HttpStatus.OK))
        .orElseThrow(() -> new BookNotFoundException(isbn));
}

We cover this functionality with two tests:

@Test
public void should_get_valid_book_with_ok_status() throws Exception {
    mockMvc.perform(get("/api/books/978-0321356680").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$.id", is(1)))
        .andExpect(jsonPath("$.title", is("Effective Java")))
        .andExpect(jsonPath("$.publisher", is("Addison Wesley")))
        .andDo(MockMvcResultHandlers.print());
}

@Test
public void should_no_get_unknown_book_with_not_found_status() throws Exception {
    mockMvc.perform(get("/api/books/000-1234567890").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isNotFound())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$[0].logref", is("error")))
        .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
        .andDo(MockMvcResultHandlers.print());
}

Update a Book

To update a book, we need to pass all the book's data using the PUT method:

@PutMapping("/{isbn}")
public ResponseEntity<Book> updateBook(@PathVariable("isbn") String isbn, @Valid @RequestBody Book book) {
    return bookRepository.findByIsbn(isbn)
        .map(bookToUpdate -> {
            bookToUpdate.setIsbn(book.getIsbn());
            bookToUpdate.setTitle(book.getTitle());
            bookToUpdate.setDescription(book.getDescription());
            bookToUpdate.setAuthors(book.getAuthors());
            bookToUpdate.setPublisher(book.getPublisher());
            bookRepository.save(bookToUpdate);

            return new ResponseEntity<>(bookToUpdate, HttpStatus.OK);
    })
    .orElseThrow(() -> new BookNotFoundException(isbn));
}

Usually, we use the PUT method to update a full entity and PATCH method to update partial element(s). We can add BookNotFoundException to the BookControllerAdvice :

@ResponseBody
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
VndErrors bookNotFoundExceptionHandler(BookNotFoundException ex) {
    return new VndErrors("error", ex.getMessage());
}

A book which is not found must reply with a 404 status code. We can see it in the test:

 @Test
public void should_not_update_unknown_book_and_return_not_found_status() throws Exception {
    Book book = new Book("978-0321356680","Book updated","Publisher");
    book.setDescription("New description");

    Author author = new Author("John","Doe");
    book.addAuthor(author);

    mockMvc.perform(put("/api/books/000-1234567890")
        .contentType(MediaType.APPLICATION_JSON)
        .content(json(book)))
        .andExpect(status().isNotFound())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$[0].logref", is("error")))
        .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
        .andDo(MockMvcResultHandlers.print());
}

Obviously, a valid update returns the book with the OK status code:

@Test
public void should_update_valid_book_and_return_ok_status() throws Exception {
    Book book = new Book("978-0321356680","Book updated","Publisher");
    book.setDescription("New description");

    Author author = new Author("John","Doe");
    book.addAuthor(author);

    mockMvc.perform(put("/api/books/978-0321356680")
        .contentType(MediaType.APPLICATION_JSON)
        .content(json(book)))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$.id", is(1)))
        .andExpect(jsonPath("$.title", is("Book updated")))
        .andExpect(jsonPath("$.description", is("New description")))
        .andExpect(jsonPath("$.publisher", is("Publisher")))
        .andExpect(jsonPath("$.authors[0].firstName", is("John")))
        .andExpect(jsonPath("$.authors[0].lastName", is("Doe")))
        .andDo(MockMvcResultHandlers.print());
}

Delete a Book

You could delete a book without worrying about its existence, but a preferable way is to inform the client that the book he's trying to delete does not exist in the database. So we need to search it before:

@DeleteMapping("/{isbn}")
public ResponseEntity<?> deleteBook(@PathVariable("isbn") String isbn) {
    return bookRepository.findByIsbn(isbn)
        .map(book -> {
            bookRepository.delete(book);
            return new ResponseEntity(HttpStatus.NO_CONTENT);
         })
         .orElseThrow(() -> new BookNotFoundException(isbn));
}

We can see the utility of BookControllerAdvice , we don't need to send the 404 HTTP code twice (update and delete book) on each method implementation. The ControllerAdvice centralizes exceptions and provides the correct status.

For the first test case, we must check there's nothing in the response body:

@Test
public void should_delete_existing_book_and_return_no_content_status() throws Exception {
    mockMvc.perform(delete("/api/books/978-0321356680")
        .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isNoContent())
        .andExpect(content().string(""))
        .andDo(MockMvcResultHandlers.print());
}

For the second test, the expected result is equivalent to the book's update test above:

@Test
public void should_not_delete_unknown_book_and_return_not_found_status() throws Exception {
    mockMvc.perform(delete("/api/books/000-1234567890")
        .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$[0].logref", is("error")))
        .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
        .andDo(MockMvcResultHandlers.print());
}

Update a Book's Description

You can update partial content of a resource. In this case, the PATCH verb is appropriated:

@PatchMapping("/{isbn}")
public ResponseEntity<Book> updateBookDescription(@PathVariable("isbn") String isbn, @RequestBody String description) {
    return bookRepository.findByIsbn(isbn)
        .map(book -> {
            book.setDescription(description);
            bookRepository.save(book);

            return new ResponseEntity<>(book, HttpStatus.OK);
        })
        .orElseThrow(() -> new BookNotFoundException(isbn));
}

So you don't need to pass all data, only data you want to update, in our case book's description. The business logic is similar to the full update so we won't waste time on it.

Here are the tests:

@Test
public void should_update_existing_book_description_and_return_ok_status() throws Exception {
    mockMvc.perform(patch("/api/books/978-0321356680")
        .contentType(MediaType.APPLICATION_JSON)
        .content("new description"))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$.title", is("Effective Java")))
        .andExpect(jsonPath("$.description", is("new description")))
        .andDo(MockMvcResultHandlers.print());
}

@Test
public void should_not_update_description_of_unknown_book_and_return_not_found_status() throws Exception {
    mockMvc.perform(patch("/api/books/000-1234567890")
        .contentType(MediaType.APPLICATION_JSON)
        .content("new description"))
        .andExpect(status().isNotFound())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$[0].logref", is("error")))
        .andExpect(jsonPath("$[0].message", containsString("could not find book with ISBN: '000-1234567890'")))
        .andDo(MockMvcResultHandlers.print());
}

Retrieve All Books

Here we go, the biggest API method implementation. It seems to be easy at first glance, but it doesn't except if you only retrieve all books with no argument. But we don't. Why?

Imagine your database has several million books. If you send all books to the clients in one call, you will have very big troubles in terms of performance dealing with all the data. So, we must paginate data.

Another trouble comes with pagination: imagine we paginate data to twenty books size, the client makes a call and get twenty books. Does it mean there are only twenty books in total or more books? That's why we must provide a means to tell the client there is more data. We use the 206 partial content status code to do that.

Look at this implementation:

@GetMapping
public ResponseEntity<List<Book>> getAllBooks(
    @PageableDefault(size = MAX_PAGE_SIZE) Pageable pageable,
    @RequestParam(required = false, defaultValue = "id") String sort,
    @RequestParam(required = false, defaultValue = "asc") String order) {
    final PageRequest pr = PageRequest.of(
        pageable.getPageNumber(), pageable.getPageSize(),
            Sort.by("asc" .equals(order) ? Sort.Direction.ASC : Sort.Direction.DESC, sort)
    );

    Page<Book> booksPage = bookRepository.findAll(pr);

    if (booksPage.getContent().isEmpty()) {
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    } else {
        long totalBooks = booksPage.getTotalElements();
        int nbPageBooks = booksPage.getNumberOfElements();

        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Total-Count", String.valueOf(totalBooks));

        if (nbPageBooks < totalBooks) {
            headers.add("first", buildPageUri(PageRequest.of(0, booksPage.getSize())));
            headers.add("last", buildPageUri(PageRequest.of(booksPage.getTotalPages() - 1, booksPage.getSize())));

            if (booksPage.hasNext()) {
                headers.add("next", buildPageUri(booksPage.nextPageable()));
            }

            if (booksPage.hasPrevious()) {
                headers.add("prev", buildPageUri(booksPage.previousPageable()));
            }

            return new ResponseEntity<>(booksPage.getContent(), headers, HttpStatus.PARTIAL_CONTENT);
        } else {
            return new ResponseEntity(booksPage.getContent(), headers, HttpStatus.OK);
        }
    }
}

Spring Data provides a way to make data pagination through a Pageable interface. I build a PageRequest object to wrap sorting and ordering abilities. It's more convenient for a client to sort data. With this object, I can make my request to the repository.

If I don't have any data, I return a 204 status code. Otherwise, I need to check if the retrieved data are partial or not. I add to the header the total books number information through the X-Total-Count header. In order to facilitate the client's life, I build pagination links and set them into the header.

So we can test all books being retrieved in one page, which returns a 200 status code:

@Test
public void should_get_all_books_with_ok_status() throws Exception {
    mockMvc.perform(get("/api/books").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(header().string("X-Total-Count", is("4")))
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$", hasSize(4)))
        .andExpect(jsonPath("$[*].id", contains(1,2,3,4)))  // sorted by id asc by default
        .andDo(MockMvcResultHandlers.print());
}

And partial content by reducing page size to split data. We also check header information:

@Test
public void should_get_first_page_paginated_books() throws Exception {
    mockMvc.perform(get("/api/books?page=0&size=2").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isPartialContent())
        .andExpect(header().string("X-Total-Count", is("4")))
        .andExpect(header().string("first", is("/api/books?page=0&size=2")))
        .andExpect(header().string("last", is("/api/books?page=1&size=2")))
        .andExpect(header().string("prev", is(nullValue())))
        .andExpect(header().string("next", is("/api/books?page=1&size=2")))
        .andExpect(header().string("X-Total-Count", is("4")))
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$", hasSize(2)))
        .andExpect(jsonPath("$[*].id", contains(1,2)))  // sorted by id asc by default
        .andDo(MockMvcResultHandlers.print());
}

@Test
public void should_get_last_page_paginated_books() throws Exception {
    mockMvc.perform(get("/api/books?page=1&size=2").contentType(MediaType.APPLICATION_JSON))
       .andExpect(status().isPartialContent())
       .andExpect(header().string("X-Total-Count", is("4")))
       .andExpect(header().string("first", is("/api/books?page=0&size=2")))
       .andExpect(header().string("last", is("/api/books?page=1&size=2")))
       .andExpect(header().string("prev", is("/api/books?page=0&size=2")))
       .andExpect(header().string("next", is(nullValue())))
       .andExpect(jsonPath("$", hasSize(2)))
       .andExpect(jsonPath("$[*].id", contains(3,4)))  // sorted by id asc by default
       .andDo(MockMvcResultHandlers.print());
}

We can check the page sorting functionality:

@Test
public void should_sort_books() throws Exception {
    mockMvc.perform(get("/api/books?sort=title&order=desc").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(header().string("X-Total-Count", is("4")))
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("$", hasSize(4)))
        .andExpect(jsonPath("$[*].id", contains(2,4,3,1)))
        .andDo(MockMvcResultHandlers.print());
}

And finally, the no content status code if there is no book:

@Test
public void should_not_get_books_for_bad_pagination() throws Exception {
    mockMvc.perform(get("/api/books?page=999").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isNoContent())
        .andDo(MockMvcResultHandlers.print());
}

Summary

In this article, we saw how it's easy to build a web service with appropriate responses. Thanks to Spring, we can map different status in a global way for our business exception without worrying about managing this on each controller endpoint.

This article shows a manner of doing proper things, but there are other manners to do this. For instance, for pagination, you could use links in the body with HATEOAS or use the link header. We must keep in mind that we have the duty to facilitate the life of the client who uses our API. Properly using HTTP status code is one way, among others.

All code can be downloaded on my GitHub account.

The State of API Integration Report provides data from the Cloud Elements platform and will help all developers navigate the recent explosion of APIs and the implications of API integrations to work more efficiently in 2017 and beyond.

Topics:
spring boot 2 ,java ,spring 5 ,rest ,integration ,web services ,restful

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}