Send It, POST-Haste
POST accepts new data into a Spring Boot REST API
Wiring up a controller method to support POST requests isn’t all that different from setting up a GET handler. However, taking data in to our API raises new considerations. How do we make sure the input meets our quality standards? How do we handle the fields required in our data layer that are not provided by the input object?
Guide to the Series: REST Fundamentals with Spring Boot
- Introduction: REST API development with Spring Boot
- GET All The Things
- Test All the Things
- Send It, POST-Haste (you are here)
- To PUT Or To PATCH
- Don’t DELETE Me Bro
Handle a POST Request
Identifying a method in the controller to handle POST requests simply requires the @PostMapping annotation, shown in the code sample below. Technically this is a composed annotation that saves a bit of typing versus the corresponding @RequestMapping(method = RequestMethod.POST) option. The @GetMapping we saw previously is a composed annotation too, and as you might guess there are similar shorthands for the remaining HTTP methods. I think the shorted annotations are also easier to read and comprehend when you’re scanning through the code looking for something.
@PostMapping
public ResponseEntity<Void> saveWorkOrder(@Valid @RequestBody WorkOrderRequest workOrderRequest,
UriComponentsBuilder uriComponentsBuilder) {
final Long workOrderId = workOrderService.createWorkOrder(workOrderRequest);
UriComponents uriComponents = uriComponentsBuilder
.path("/api/v1/work-orders/{id}")
.buildAndExpand(workOrderId);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(uriComponents.toUri());
return new ResponseEntity<>(headers, HttpStatus.CREATED);
}
There are several things to note about the implementation of this method, but we will begin with the method body. The real purpose of this method is fulfilled with the use of the WorkOrderService to create a WorkOrder entity from the WorkOrderRequest input object. The details of this service are outside the scope of this article series, but it basically comes down to the use of MapStruct to convert the input type into an Entity, and Spring Data JPA to persist the Entity to the database.
WorkOrderRequest … WorkOrderDto … WorkOrder … What are we doing?
Defining separate object types for persistence versus other layers is often useful, allowing the types to address slightly different concerns. In this project, there are different validation needs between existing Work Orders represented by WorkOrderDto, and a request to create a new Work Order represented by WorkOrderRequest. Since MapStruct makes converting the types simple, there is nearly no downside to this approach and the validation becomes much simpler.
The next few lines of the method body prepare the Location response header value to inform the client as to where the saved result can be obtained. In other words, the controller is saying "OK, we saved your WorkOrder and here is where you can find it." This is accomplished with the UriComponentsBuilder which is injected by Spring Framework. You’ll note the use of an overload to the ResponseEntity constructor which takes the HttpHeaders parameter where we created the Location header. Returning a 201 HTTP status code is the accepted practice following a successful POST, so we use HttpStatus.CREATED as well.
Now, back up and take a look at the annotations on the WorkOrderRequest method parameter. The @RequestBody annotation indicates this parameter will be bound from the incoming request content. Since this is a complex type it wouldn’t be practical to look for it on a query string.
The @Valid annotation is part of Bean Validation and relates to annotations placed on the type is relates to. If you review the WorkOrderDto class code below, you’ll note the fields of the class are annotated with @Null or @NotEmpty, and these annotations define acceptable input states for this type.
Bean Validation
At the beginning of this article, I posed the question "How do we make sure the input meets our quality standards?" Bean Validation is a key aspect of achieving the data input quality we need. This is done by using annotations on fields of the WorkOrderRequest. Below, you can see the use of @NotEmpty on several fields. This annotation ensures the value is neither null nor empty.
You can add support for Bean Validation to your Spring Boot project via the spring-boot-starter-validation dependency, which includes the reference implementation, Hibernate Validator.
Another annotation that can be useful is the @Null annotation. This ensures that incoming data values will not be bound when passed from a client and instead will be a validation error. For example, if an audit Timestamp field was included on the object, but you wanted to prevent the client from setting this value, the @Null annotation would accomplish this.
Input Object
package com.springbikeclinic.api.web.model;
import lombok.Builder;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@Data
@Builder
public class WorkOrderRequest {
@NotEmpty
private String bikeDescription;
@NotEmpty
private String workDescription;
@NotEmpty
private String status;
private String mechanicNotes;
}
The other question I posed, "How do we handle the fields required in our data layer that are not provided by the input object?", is related to the Long id and Timestamp createdDateTime fields on the entity object. There are no corresponding fields for these values on the WorkOrderRequest object.
As seen in the code below, the @ID and @GeneratedValue annotations from the Java Persistence API are used on the ID field. This correlates to the primary key when the entity is persisted to the database.
The @CreationTimestamp annotation is from Hibernate and is a great time saver (and code saver) for this common scenario.
From the Hibernate documentation:
The @CreationTimestamp annotation instructs Hibernate to set the annotated entity attribute with the current timestamp value of the JVM when the entity is being persisted.
ENTITY OBJECT
package com.springbikeclinic.api.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.sql.Timestamp;
@Entity
@Table(name = "work_orders")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class WorkOrder {
@Id
@GeneratedValue
private Long id;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private Timestamp createdDateTime;
private String bikeDescription;
private String workDescription;
private String status;
private String mechanicNotes;
}
What About Tests?
We’ll start with the happy path by posting a valid input object. In our test class, we autowired an instance of Jackson’s ObjectMapper which makes it easy to obtain a JSON string to use as input to the POST operation.
As we have previously covered, we use Mockito to define what the mock WorkOrderService should do, via the static Mockito.when(T methodCall) and thenReturn(T value) calls.
Lastly, we also use Mockito to verify the expected interaction with the WorkOrderService. Because this test exercises the controller using valid input, we expect the controller to pass the input object to the service instance. Note that we are not actually testing the service here, that would be a concern for a different test. We are only verifying behavior of the controller here, which is why WorkOrderService is a mock and not a real instance.
@Test
void postWorkOrder_validInput_isCreated() throws Exception {
String jsonWorkOrderRequest = objectMapper.writeValueAsString(workOrderRequest);
when(workOrderService.createWorkOrder(any(WorkOrderDto.class))).thenReturn(123L);
mockMvc.perform(post(WORK_ORDERS_BASE_PATH)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWorkOrderRequest))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"));
verify(workOrderService, times(1)).createWorkOrder(any(WorkOrderDto.class));
}
It is also a good idea to test with invalid input, to confirm the bean validation described earlier in this article. For this we POST a WorkOrderRequest without the workDescription and status fields populated.
We expect an HTTP 400 response from the controller in this scenario. Assertions on the response body can also be performed using JsonPath which is included in our project via the spring-boot-starter-test artifact defined in our Maven POM. As a simple example, here we verify the presence of a specific error message for each of the two fields we omitted to trigger the 400 response.
Finally, we expect the bean validation to prevent the input from being processed by the WorkOrderService, so we use Mockito to make sure no calls were made to the service.
@Test
void postWorkOrder_withIncompleteWorkOrderRequest_isBadRequest() throws Exception {
String jsonWorkOrderRequest = objectMapper.writeValueAsString(
WorkOrderRequest.builder()
.bikeDescription("Incomplete Bike")
.build());
mockMvc.perform(post(WORK_ORDERS_BASE_PATH)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWorkOrderRequest))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$[0].defaultMessage", is("must not be empty")))
.andExpect(jsonPath("$[1].defaultMessage", is("must not be empty")));
verifyNoInteractions(workOrderService);
}
Are We There Yet?
We now have a solid start to our REST API, with support for basic GET and POST operations. However, you might be wondering how we can update existing work orders. Existing work orders will have assigned id and createdDateTime values, but these fields do not exist in our POST request. You raise a good question, and one which takes us to the next chapter in our story.