This is the sixth post in a series of posts to cover different aspects of Spring Boot. Please note that the entire post isn’t necessarily only written in English.
In this post, I am going to cover how to define REST endpoints in Spring, and auto generate repository-based REST endpoints with Spring Data REST. This post describes how to produce REST services, and in the next post, we will talk about how to consume REST services.
Summary
- Controller handler methods can either be annotated with
@ResponseBody
, or returnResponseEntity
objects to bypass the model & view, and write data directly to the response body. However if we use@RestController
, we don’t need these.
- Spring HATEOAS enables hyperlinking of resources returned from Spring MVC controllers.
- Spring Data repositories can automatically expose REST APIs via Spring Data REST.
Rest Controllers
Spring MVC creates traditional MPA (MultiPage Application), while Angular creates SPA (Single Page Application).
Since presentation is largely decoupled from backend processing in a SPA, it affords the opportunity to develop more than one UI (such as a native mobile application) for the same backend functionality. It can also integrate with other applications that can consume the API. But MPA is a simpler design if all you need is to display information on a web page.
The Angular client code will communicate with an API via HTTP requests.
Get
Angular code for GET:
1 | import { Component, OnInit, Injectable } from '@angular/core'; |
Controller to GET recent designs:
1 |
|
@RestController = @Controller + @ResponseBody
- Marks a class for auto discovery by component scanning
- All returned value should be written directly to the response body.
Offer an endpoint that GET a single taco by ID
1 | // 注意: @PathVariable |
Problem:
If we return null
, the client receives a response with an empty body and 200 OK. The response cannot be used (since it’s empty), while the status indicates that everything is fine. So it’s better to return 404 for null
.
Improvement: Use ResponseEntity
to indicate status
1 |
|
Post
Angular code for POST
1 | // onSubmit calls httpClient.POST |
Controller to POST client input
1 | // 注意,这里用的是 consumes,因为 receive input from client |
@RequestBody
: Ensures that JSON in the request body is bound to the Taco
object. Without @RequestBody
, Spring will bound request parameters to the Taco
object.
@ResponseStatus
: Communicate with more descriptive and accurate status to the client.
Put & Patch
It’s true that PUT is often used to update resource data, but PUT is actually the semantic opposite of GET . GET requests are for transferring data from the server to the client, and PUT requests are for sending data from the client to the server.
In a sense, PUT is really intended to perform a wholesale replacement operation rather than an update operation. In contrast, the purpose of PATCH is to perform a patch or partial update of resource data.
Suppose we want to change the order’s address. One way is to use a PUT request like this:
1 |
|
Note: The request path. This is the same way paths are handled by GET.
It would work, but it requires the client to submit the complete data in the PUT request. If any of the order’s properties are omitted, that property’s value would be overwritten to null
.
In this case, we will use PATCH for a partial update.
1 |
|
Even though PATCH semantically implies a partial update, it’s up to us to write code in the handler method that actually performs such an update. PATCH allows client to only send the properties that should be changed, and enables the server to retain other existing data.
However, PATCH has the following limitations:
- If
null
means no change, then how will the client indicate that a field should be set tonull
? - There’s no way of removing / adding a subset of items in a collection. If client wants to modify a collection, the entire altered collection should be sent.
Delete
Delete an order:
1 |
|
Why catch that exception and do nothing with it: If you delete a resource that doesn’t exist, the outcome is the same as deleting it, so whether it existed before or not doesn’t matter.
HTTP 204: Typically, DELETE requests have no body, and should only communicate the status code to let the client know not to expect any content.
Comparison
Compare 5 types of HTTP requests in the controller:
GET 1: Pagination
1 | // Get recent designs |
GET 2: Response Entity
1 | // Get signle taco by ID |
1 | // Post client input |
1 |
|
1 |
|
1 |
|
Using Hypermedia
Hardcoding API URLs and using string manipulation on them makes the client code brittle. Hence HATEOAS (Hypermedia as the Engine of Application State) is for creating self-describing APIs, where resources returned from an API contain links to related
resources.
If the API is enabled with hypermedia, the API will describe its own URLs, relieving the client of needing to be hardcoded with that knowledge.
This enables clients to navigate an API with minimal understanding of the API’s URLs. Instead, it understands relationships between the resources served by the API, and uses understanding of relationships to discover the API’s URLs, as it traverses those relationships.
Getting Started
First, add HATEOAS to the dependency:
1 | <dependency> |
A comparison between non-Hateoas & Hateoas:
The second one with hyperlinks is known as HAL (Hypertext Application Language), a simple and commonly used format for embedding hyperlinks in JSON responses.
Normal JSON:
1 | [ |
With Hyperlink:
Each element includes a property named _links
that contains hyperlinks for the client to nagivate the API.
Should a client application need to perform an HTTP request against a taco in the list, it doesn’t need to be developed with any knowledge of the taco resource’s URL. Instead, it knows to ask for the self link, which maps to http://localhost:8080/design/4. If the client wants to deal with a particular ingredient, it only needs to follow the self link for that ingredient.
1 | { |
Adding Hyperlinks
Spring HATEOAS provides two primary types that represent hyperlinked resources: Resource and Resources. The Resource type represents a single resource, whereas Resources is a collection of resources. When returned from a REST controller method, the links will be included in the JSON received by the client.
The original controller:
1 |
|
The original implementation returned a List<Taco>
, which was fine at the time. But we need to return a Resources
object instead. Below is the first step:
1 |
|
In this new version of recentTacos()
, we no longer return the list of tacos directly. Instead, we use Resources.wrap()
to wrap the list of tacos as an instance of Resources<Resource<Taco>>
, which is ultimately returned from the method.
Before returning the Resources object, we add a link whose relationship name is recents
, and whose URL is http://localhost:8080/design/recent. The following JSON is included in the resource returned from the API request:
1 | "_links": { |
At this point, the only added link is to the entire list; No links are added to the taco resources themselves, or to the ingredients of each taco. But first, we need to get rid of the hardcoded localhost:8080
.
The Spring HATEOAS link builder provides ControllerLinkBuilder
, and it knows what the hostname is without having to hardcode it.
1 |
|
Now we get rid of the hardcoded hostname, and we also don’t have to specify the /design
path. Instead, we ask for a link to DesignTacoController
, whose base path is /design
. ControllerLinkBuilder
uses the controller’s base path as the foundation of the Link object we’re creating.
Now we move one step further, and get rid of the /recent
path as well. We can call linkTo()
by giving it a method on the controller to have ControllerLinkBuilder
derive the base URL from both the controller’s base path and the method’s mapped path:
1 |
|
Now the entire URL is derived from the controller’s mappings. The methodOn()
takes the controller class, and lets us make a call to the recentTacos()
method, which is intercepted by ControllerLinkBuilder
and used to determine both the controller’s base path and also the path mapped to recentTacos()
.
Creating Resource Assemblers
Now we need to add links to the resource in the list.
One option is to loop through each of the Resource<Taco>
elements in the Resources object, adding a Link to each individually. But we need to repeat the loop in the API wherever we return a list of taco resources.
Hence we’re going to define a utility class that converts Taco
objects to a new TacoResource
object. The new TacoResource
object will be able to carry links.
1 | // TacoResource: A Resource, Not a Domain! |
Extends ResourceSupport
: Inherit a list of Link
objects and methods, to manage list of links.
Note: TacoResource
does not include the id
property from Taco
. There’s no need to expose any database-specific IDs in the API. The resource’s self
link will serve as the resource identifier.
Domain & Resource:
Create a separate resource type, so that Taco
isn’t unnecessarily cluttered with resource links where links aren’t needed. Also, leave the id
property out so it won’t be exposed to the API.
Create a resource assembler:
1 | // Extend the superclass |
The default constructor: Informs the superclass ( ResourceAssemblerSupport
) that it will use DesignTacoController
to determine the base path for any URLs in links it creates, when creating a TacoResource
.
The instantiateResource()
method is overridden: Given a Taco
, instantiate a TacoResource
. This method would be optional if TacoResource
had a default constructor. In this case, TacoResource
requires construction with a Taco , so we’re required to override it.
The toResource()
method is the only method that’s strictly mandatory when extending ResourceAssemblerSupport
. Here we create a TacoResource
object from a Taco
, and automatically give it a self link with the derived URL from the id
property.
On the surface, toResource()
seems similar to instantiateResource()
, but they’re different. Under the covers, toResource()
will call instantiateResource()
.instantiateResource()
only instantiates a Resource object, while toResource()
both creates the Resource object, and populates it with links.
Now, use TacoResourceAssembler
in the controller:
1 |
|
Rather than return Resources<Resource<Taco>>
, recentTacos()
now returns Resources<TacoResource>
to take advantage of the new TacoResource
type.
After fetching the tacos from the repository, we pass the list of Taco objects to the toResources()
method on a TacoResourceAssembler
, which will cycle through all Taco
objects, calling the overridden toResource
method to create a list of TacoResource
objects.
With that TacoResource
list, we then create a Resources<TacoResource>
object, and populate it with the recents
links.
At this point, a GET request to /design/recent
will produce a list of tacos, each with a self link and a recents
link on the list itself. But the ingredients will still be without a link. Therefore, we first create the IngredientResource
object:
1 | public class IngredientResource extends ResourceSupport { |
Then create a new resource assembler for ingredients:
1 | class IngredientResourceAssembler extends ResourceAssemblerSupport<Ingredient, IngredientResource> { |
Slightly change TacoResource
, so it carries IngredientResource
objects instead of Ingredient
objects:
1 | public class TacoResource extends ResourceSupport { |
Now the recent tacos list is completely outfitted with hyperlinks, not only for the recents
link, but also for all of its taco entries and the ingredients of those tacos. The response should look a lot like the JSON in here.
Naming Embedded Relations
If we take a closer look at the JSON in here. we notice that the top-level elements look like this:
1 | { |
The name tacoResourceList
was based on creating Resources
object from List<TacoResources>
. If we were to refactor the
name of the TacoResource
class to something else, the field name in the resulting JSON would change to match it. This would likely break the clients code that were counted on that name.
To tackle this problem, we use @Relation
to help break the coupling between JSON field name and resource type class names.
1 |
|
Here we’ve specified that when a list of TacoResource
objects is used in a Resources object, it should be named tacos. And a single TacoResource
object should be referred to in JSON as taco . Now the returned JSON looks like this:
1 | { |
Controller Evolution
The evolution of a controller method with HATEOAS:
Return list of tacos directly.
1 | // Get recent designs, with pagination |
Wrap the list, but still has hardcoded URL.
1 |
|
Partially get rid of the hardcoded URL.
1 |
|
Get rid of all hardcoded URL.
1 |
|
Add Resource Assembler.
1 |
|
Spring Data REST
Spring HATEOAS makes adding links to your API rather straightforward and simple, but it did add several lines of code that we otherwise do not need. However, if we want to be lazy, then let’s see how Spring Data REST can automatically create APIs based on the data repositories we’ve created.
With Spring Data REST, there’s no need to create Rest Controllers.
Spring Data REST is another member of the Spring Data family that automatically creates REST APIs for repositories created by Spring Data. By only adding Spring Data REST to the build, we get an exposed REST API with operations for each repository interface we’ve defined.
1 | <dependency> |
We need to set a base path for the API, so the endpoints are all distinct:
1 | spring.data.rest.basepath: /api |
And make requests such as:
1 | curl localhost:8080/api |
Adjust Resource Paths & Relation Names
When creating endpoints for Spring Data repositories, Spring Data REST will pluralize the associated entity class. For instance, /users
for User Entity, /tacoes
for Taco Entity (tacoes, not tacos).
However, the correct spelling should be tacos
. Hence we modify the Taco
Entity class by adding the following annotation:
1 |
|
Setting this would give the result:
1 | "tacos" : { |
Paging & Sorting
We noticed that the link above contains paths to page
, size
, and sort
. By default, requests to a collection resource such as /api/tacos
will return 20 items per page. But we can customize it:
1 | # 5 items per page, only dispay the 2nd page(页数从0算起) |
And HATEOAS will offer links for the first, last, next, previous pages in the response.
The sort
parameter lets us sort the result. For instance, we need to fetch the 12 most recently created tacos:
1 | curl localhost:8080/api/tacos?sort=createdAt,desc&page=0&size=12 |
However the only problem left is that, the UI client still uses hardcoded request. So it’s better to make the client look up the URL from a list of links, hence we need to add custom endpoints & links.
Add Custom Endpoints & Links
When we write our own API controllers along with the ones provided by Spring Data REST, there are 2 problems:
- Our own controller endpoints aren’t mapped under Spring Data REST’s base path. We can force mappings to be prefixed with the base path. But if the base path changes, we need to also change the base path in the controllers.
- Any endpoints defined in the controllers won’t be automatically included as hyperlinks. This means that clients won’t be able to discover the custom endpoints with a relation name.
We first solve the problem of the base path. Spring Data REST includes a new controller called @RepositoryRestController
. It annotates the controller class, and all mapping will have the same base path as in Spring Data REST.
Then we dump the old DesignTacoController
and create the new controller that only contains recentTacos()
method:
1 | // Make sure the path will be: /api/tacos/recent |
Note:
@RepositoryRestController
doesn’t assures that returned values are written to the response body, hence we need to either addResponseEntity
, or use the class-level@ResponseBody
.
Now we deal with the hyperlinks when clients search for relations, to make the custom endpoints appear in the hyperlinks list when client requests /api/tacos
. In this case, we need a resource processor bean.
1 |
|
Spring HATEOAS will auto discover the ResrouceProcessor
Bean(s), and will apply them to the appropriate resources.
In this case, if a PagedResources<Resource<Taco>>
is returned from a controller, it will receive a link for the most recently created tacos. This includes the response from requests for /api/tacos
.