Chapter 6. Creating REST services

This chapter covers

  • Defining REST endpoints in Spring MVC
  • Enabling hyperlinked REST resources
  • Automatic repository-based REST endpoints

“The web browser is dead. What now?”

Roughly a dozen years ago, I heard someone suggest that the web browser was nearing legacy status and that something else would take over. But how could this be? What could possibly dethrone the near-ubiquitous web browser? How would we consume the growing number of sites and online services if not with a web browser? Surely these were the ramblings of a madman!

Fast-forward to the present day and it’s clear that the web browser hasn’t gone away. But it no longer reigns as the primary means of accessing the internet. Mobile devices, tablets, smart watches, and voice-based devices are now commonplace. And even many browser-based applications are actually running JavaScript applications rather than letting the browser be a dumb terminal for server-rendered content.

With such a vast selection of client-side options, many applications have adopted a common design where the user interface is pushed closer to the client and the server exposes an API through which all kinds of clients can interact with the backend functionality.

In this chapter, you’re going to use Spring to provide a REST API for the Taco Cloud application. You’ll use what you learned about Spring MVC in chapter 2 to create RESTful endpoints with Spring MVC controllers. You’ll also automatically expose REST endpoints for the Spring Data repositories you defined in chapter 4. Finally, we’ll look at ways to test and secure those endpoints.

But first, you’ll start by writing a few new Spring MVC controllers that expose backend functionality with REST endpoints to be consumed by a rich web frontend.

6.1. Writing RESTful controllers

I hope you don’t mind, but while you were turning the page and reading the introduction to this chapter, I took it upon myself to reimagine the user interface for Taco Cloud. What you’ve been working with has been fine for getting started, but it lacked in the aesthetics department.

Figure 6.1 is just a sample of what the new Taco Cloud looks like. Pretty snazzy, huh?

Figure 6.1. The new Taco Cloud home page

And while I was spiffing up the Taco Cloud look, I also decided to build the frontend as a single-page application using the popular Angular framework. Ultimately, this new browser UI will replace the server-rendered pages you created in chapter 2. But for that to work, you’ll need to create a REST API that the Angular-based[1] UI will communicate with to save and fetch taco data.

1

I chose to use Angular, but the choice of frontend framework should have little to no bearing on how the backend Spring code is written. Feel free to choose Angular, React, Vue.js, or whatever frontend technology suits you best.

To SPA or not to SPA?

You developed a traditional multipage application (MPA) with Spring MVC in chapter 2, and now you’re replacing that with a single-page application (SPA) based on Angular. But I’m not suggesting that SPA is always a better choice than MPA.

Because presentation is largely decoupled from backend processing in a SPA, it affords the opportunity to develop more than one user interface (such as a native mobile application) for the same backend functionality. It also opens up the opportunity for integration with other applications that can consume the API. But not all applications require that flexibility, and MPA is a simpler design if all you need is to display information on a web page.

This isn’t a book on Angular, so the code in this chapter will focus primarily on the backend Spring code. I’ll show just enough Angular code to give you a feel for how the client side works. Rest assured that the complete set of code, including the Angular frontend, is available as part of the downloadable code for the book and at https://github.com/habuma/spring-in-action-5-samples. You may also be interested in reading Angular in Action by Jeremy Wilken (Manning, 2018) and Angular Development with TypeScript, Second Edition by Yakov Fain and Anton Moiseev (Manning, 2018).

In a nutshell, the Angular client code will communicate with an API that you’ll create throughout this chapter by way of HTTP requests. In chapter 2 you used @GetMapping and @PostMapping annotations to fetch and post data to the server. Those same annotations will still come in handy as you define your REST API. In addition, Spring MVC supports a handful of other annotations for various types of HTTP requests, as listed in table 6.1.

Table 6.1. Spring MVC’s HTTP request-handling annotations

Annotation

HTTP method

Typical use[a]

@GetMapping HTTP GET requests Reading resource data
@PostMapping HTTP POST requests Creating a resource
@PutMapping HTTP PUT requests Updating a resource
@PatchMapping HTTP PATCH requests Updating a resource
@DeleteMapping HTTP DELETE requests Deleting a resource
@RequestMapping General purpose request handling; HTTP method specified in the method attribute  

a

Mapping HTTP methods to create, read, update, and delete (CRUD) operations isn’t a perfect match, but in practice, that’s how they’re often used and how you’ll use them in Taco Cloud.

To see these annotations in action, you’ll start by creating a simple REST endpoint that fetches a few of the most recently created tacos.

6.1.1. Retrieving data from the server

One of the coolest things about Taco Cloud is that it allows taco fanatics to design their own taco creations and share them with their fellow taco lovers. To this end, Taco Cloud needs to be able to display a list of the most recently created tacos when the Latest Designs link is clicked.

In the Angular code I’ve defined a RecentTacosComponent that will display the most recently created tacos. The complete TypeScript code for RecentTacosComponent is shown in the next listing.

Listing 6.1. Angular component for displaying recent tacos
import { Component, OnInit, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'recent-tacos',
  templateUrl: 'recents.component.html',
  styleUrls: ['./recents.component.css']
})

@Injectable()
export class RecentTacosComponent implements OnInit {
  recentTacos: any;

  constructor(private httpClient: HttpClient) { }

  ngOnInit() {
    this.httpClient.get('http://localhost:8080/design/recent')       1
        .subscribe(data => this.recentTacos = data);
  }
}

  • 1 Fetches recent tacos from the server

Turn your attention to the ngOnInit() method. In that method, RecentTacosComponent uses the injected Http module to perform an HTTP GET request to http://localhost:8080/design/recent, expecting that the response will contain a list of taco designs, which will be placed in the recentTacos model variable. The view (in recents.component.html) will present that model data as HTML to be rendered in the browser. The end result might look something like figure 6.2, after three tacos have been created.

Figure 6.2. Displaying the most recently created tacos

The missing piece in this puzzle is an endpoint that handles GET requests for /design/ recent and responds with a list of recently designed tacos. You’ll create a new controller to handle such a request. The next listing shows the controller for the job.

Listing 6.2. A RESTful controller for taco design API requests
package tacos.web.api;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.hateoas.EntityLinks;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import tacos.Taco;
import tacos.data.TacoRepository;

@RestController
@RequestMapping(path="/design",                    1
                produces="application/json")
@CrossOrigin(origins="*")                          2
public class DesignTacoController {
  private TacoRepository tacoRepo;
  @Autowired
  EntityLinks entityLinks;

  public DesignTacoController(TacoRepository tacoRepo) {
    this.tacoRepo = tacoRepo;
  }

  @GetMapping("/recent")
  public Iterable<Taco> recentTacos() {            3
    PageRequest page = PageRequest.of(
            0, 12, Sort.by("createdAt").descending());
    return tacoRepo.findAll(page).getContent();
  }
}

  • 1 Handles requests for /design
  • 2 Allows cross-origin requests
  • 3 Fetches and returns recent taco designs

The findAll() method called on TacoRepository in Listing 6.2 accepts a PageRequest object to perform paging. This particular findAll() method is made available by changing TacoRepository so it extends PagingAndSortingRepository instead of CrudRepository.

You may be thinking that this controller’s name sounds familiar. In chapter 2 you created a DesignTacoController that handled similar types of requests. But where that controller was for the multipage Taco Cloud application, this new DesignTacoController is a REST controller, as indicated by the @RestController annotation.

The @RestController annotation serves two purposes. First, it’s a stereotype annotation like @Controller and @Service that marks a class for discovery by component scanning. But most relevant to the discussion of REST, the @RestController annotation tells Spring that all handler methods in the controller should have their return value written directly to the body of the response, rather than being carried in the model to a view for rendering.

Alternatively, you could have annotated DesignTacoController with @Controller, just like with any Spring MVC controller. But then you’d need to also annotate all of the handler methods with @ResponseBody to achieve the same result. Yet another option would be to return a ResponseEntity object, which we’ll discuss in a moment.

The @RequestMapping annotation at the class level works with the @GetMapping annotation on the recentTacos() method to specify that the recentTacos() method is responsible for handling GET requests for /design/recent (which is exactly what your Angular code needs).

You’ll notice that the @RequestMapping annotation also sets a produces attribute. This specifies that any of the handler methods in DesignTacoController will only handle requests if the request’s Accept header includes “application/json”. Not only does this limit your API to only producing JSON results, it also allows for another controller (perhaps the DesignTacoController from chapter 2) to handle requests with the same paths, so long as those requests don’t require JSON output. Even though this limits your API to being JSON-based (which is fine for your needs), you’re welcome to set produces to an array of String for multiple content types. For example, to allow for XML output, you could add “text/html” to the produces attribute:

@RequestMapping(path="/design",
                produces={"application/json", "text/xml"})

The other thing you may have noticed in listing 6.2 is that the class is annotated with @CrossOrigin. Because the Angular portion of the application will be running on a separate host and/or port from the API (at least for now), the web browser will prevent your Angular client from consuming the API. This restriction can be overcome by including CORS (Cross-Origin Resource Sharing) headers in the server responses. Spring makes it easy to apply CORS with the @CrossOrigin annotation. As applied here, @CrossOrigin allows clients from any domain to consume the API.

The logic within the recentTacos() method is fairly straightforward. It constructs a PageRequest object that specifies that you only want the first (0th) page of 12 results, sorted in descending order by the taco’s creation date. In short, you want a dozen of the most recently created taco designs. The PageRequest is passed into the call to the findAll() method of TacoRepository, and the content of that page of results is returned to the client (which, as you saw in listing 6.1, will be used as model data to display to the user).

Now let’s say that you want to offer an endpoint that fetches a single taco by its ID. By using a placeholder variable in the handler method’s path and accepting a path variable, you can capture the ID and use it to look up the Taco object through the repository:

@GetMapping("/{id}")
public Taco tacoById(@PathVariable("id") Long id) {
  Optional<Taco> optTaco = tacoRepo.findById(id);
  if (optTaco.isPresent()) {
    return optTaco.get();
  }
  return null;
}

Because the controller’s base path is /design, this controller method handles GET requests for /design/{id}, where the {id} portion of the path is a placeholder. The actual value in the request is given to the id parameter, which is mapped to the {id} placeholder by @PathVariable.

Inside of tacoById(), the id parameter is passed to the repository’s findById() method to fetch the Taco. findById() returns an Optional<Taco> because there may not be a taco with the given ID. Therefore, you need to determine whether the ID matched a taco or not before returning a value. If it matches, you call get() on the Optional<Taco> object to return the actual Taco.

If the ID doesn’t match any known tacos, you return null. This, however, is less than ideal. By returning null, the client receives a response with an empty body and an HTTP status code of 200 (OK). The client is handed a response it can’t use, but the status code indicates everything is fine. A better approach would be to return a response with an HTTP 404 (NOT FOUND) status.

As it’s currently written, there’s no easy way to return a 404 status code from tacoById(). But if you make a few small tweaks, you can set the status code appropriately:

@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {
  Optional<Taco> optTaco = tacoRepo.findById(id);
  if (optTaco.isPresent()) {
    return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);
  }
  return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}

Now, instead of returning a Taco object, tacoById() returns a ResponseEntity<Taco>. If the taco is found, you wrap the Taco object in a ResponseEntity with an HTTP status of OK (which is what the behavior was before). But if the taco isn’t found, you wrap a null in a ResponseEntity along with an HTTP status of NOT FOUND to indicate that the client is trying to fetch a taco that doesn’t exist.

You now have the start of a Taco Cloud API for your Angular client—or any other kind of client, for that matter. For development testing purposes, you may also want to use command-line utilities like curl or HTTPie (https://httpie.org/) to poke about the API. For example, the following command line shows how you might fetch recently created tacos with curl:

$ curl localhost:8080/design/recent

Or like this if you prefer HTTPie:

$ http :8080/design/recent

But defining an endpoint that returns information is only the start. What if your API needs to receive data from the client? Let’s see how you can write controller methods that handle input on the requests.

6.1.2. Sending data to the server

So far your API is able to return a dozen of the most recently created tacos. But how did those tacos get created in the first place?

You haven’t deleted any code from chapter 2 yet, so you still have the original DesignTacoController that displays a taco design form and handles form submission. That’s a great way to get some test data in place to test the API you’ve created. But if you’re going to transform Taco Cloud into a single-page application, you’ll need to create Angular components and corresponding endpoints to replace that taco design form from chapter 2.

I’ve already handled the client code for the taco design form by defining a new Angular component named DesignComponent (in a file named design.component.ts). As it pertains to handling form submission, DesignComponent has an onSubmit() method that looks like this:

onSubmit() {
  this.httpClient.post(
      'http://localhost:8080/design',
      this.model, {
          headers: new HttpHeaders().set('Content-type', 'application/json'),
      }).subscribe(taco => this.cart.addToCart(taco));

  this.router.navigate(['/cart']);
}

In the onSubmit() method, the post() method of HttpClient is called instead of get(). This means that instead of fetching data from the API, you’re sending data to the API. Specifically, you’re sending a taco design, which is held in the model variable, to the API endpoint at /design with an HTTP POST request.

This means that you’ll need to write a method in DesignTacoController to handle that request and save the design. By adding the following postTaco() method to DesignTacoController, you enable the controller to do exactly that:

@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
  return tacoRepo.save(taco);
}

Because postTaco() will handle an HTTP POST request, it’s annotated with @PostMapping instead of @GetMapping. You’re not specifying a path attribute here, so the postTaco() method will handle requests for /design as specified in the class-level @RequestMapping on DesignTacoController.

You do set the consumes attribute, however. The consumes attribute is to request input what produces is to request output. Here you use consumes to say that the method will only handle requests whose Content-type matches application/json.

The Taco parameter to the method is annotated with @RequestBody to indicate that the body of the request should be converted to a Taco object and bound to the parameter. This annotation is important—without it, Spring MVC would assume that you want request parameters (either query parameters or form parameters) to be bound to the Taco object. But the @RequestBody annotation ensures that JSON in the request body is bound to the Taco object instead.

Once postTaco() has received the Taco object, it passes it to the save() method on the TacoRepository.

You may have also noticed that I’ve annotated the postTaco() method with @ResponseStatus(HttpStatus.CREATED). Under normal circumstances (when no exceptions are thrown), all responses will have an HTTP status code of 200 (OK), indicating that the request was successful. Although an HTTP 200 response is always welcome, it’s not always descriptive enough. In the case of a POST request, an HTTP status of 201 (CREATED) is more descriptive. It tells the client that not only was the request successful, but a resource was created as a result. It’s always a good idea to use @ResponseStatus where appropriate to communicate the most descriptive and accurate HTTP status code to the client.

Although you’ve used @PostMapping to create a new Taco resource, POST requests can also be used to update resources. Even so, POST requests are typically used for resource creation and PUT and PATCH requests are used to update resources. Let’s see how you can update data using @PutMapping and @PatchMapping.

6.1.3. Updating data on the server

Before you write any controller code for handling HTTP PUT or PATCH commands, you should take a moment to consider the elephant in the room: Why are there two different HTTP methods for updating resources?

Although it’s true that PUT is often used to update resource data, it’s actually the semantic opposite of GET. Whereas GET requests are for transferring data from the server to the client, PUT requests are for sending data from the client to the server.

In that sense, PUT is really intended to perform a wholesale replacement operation rather than an update operation. In contrast, the purpose of HTTP PATCH is to perform a patch or partial update of resource data.

For example, suppose you want to be able to change the address on an order. One way we could achieve this through the REST API is with a PUT request handled like this:

@PutMapping("/{orderId}")
public Order putOrder(@RequestBody Order order) {
  return repo.save(order);
}

This could work, but it would require that the client submit the complete order data in the PUT request. Semantically, PUT means “put this data at this URL,” essentially replacing any data that’s already there. If any of the order’s properties are omitted, that property’s value would be overwritten with null. Even the tacos in the order would need to be set along with the order data or else they’d be removed from the order.

If PUT does a wholesale replacement of the resource data, then how should you handle requests to do just a partial update? That’s what HTTP PATCH requests and Spring’s @PatchMapping are good for. Here’s how you might write a controller method to handle a PATCH request for an order:

@PatchMapping(path="/{orderId}", consumes="application/json")
public Order patchOrder(@PathVariable("orderId") Long orderId,
                        @RequestBody Order patch) {

  Order order = repo.findById(orderId).get();
  if (patch.getDeliveryName() != null) {
    order.setDeliveryName(patch.getDeliveryName());
  }
  if (patch.getDeliveryStreet() != null) {
    order.setDeliveryStreet(patch.getDeliveryStreet());
  }
  if (patch.getDeliveryCity() != null) {
    order.setDeliveryCity(patch.getDeliveryCity());
  }
  if (patch.getDeliveryState() != null) {
    order.setDeliveryState(patch.getDeliveryState());
  }
  if (patch.getDeliveryZip() != null) {
    order.setDeliveryZip(patch.getDeliveryState());
  }
  if (patch.getCcNumber() != null) {
    order.setCcNumber(patch.getCcNumber());
  }
  if (patch.getCcExpiration() != null) {
    order.setCcExpiration(patch.getCcExpiration());
  }
  if (patch.getCcCVV() != null) {
    order.setCcCVV(patch.getCcCVV());
  }

  return repo.save(order);
}

The first thing to note here is that the patchOrder() method is annotated with @PatchMapping instead of @PutMapping, indicating that it should handle HTTP PATCH requests instead of PUT requests.

But the one thing you’ve no doubt noticed is that the patchOrder() method is a bit more involved than the putOrder() method. That’s because Spring MVC’s mapping annotations, including @PatchMapping and @PutMapping, only specify what kinds of requests a method should handle. These annotations don’t dictate how the request will be handled. Even though PATCH semantically implies a partial update, it’s up to you to write code in the handler method that actually performs such an update.

In the case of the putOrder() method, you accepted the complete data for an order and saved it, adhering to the semantics of HTTP PUT. But in order for patchMapping() to adhere to the semantics of HTTP PATCH, the body of the method requires more intelligence. Instead of completely replacing the order with the new data sent in, it inspects each field of the incoming Order object and applies any non-null values to the existing order. This approach allows the client to only send the properties that should be changed and enables the server to retain existing data for any properties not specified by the client.

There’s more than one way to PATCH

The patching approach applied in the patchOrder() method has a couple of limitations:

  • If null values are meant to specify no change, how can the client indicate that a field should be set to null?
  • There’s no way of removing or adding a subset of items from a collection. If the client wants to add or remove an entry from a collection, it must send the complete altered collection.

There’s really no hard-and-fast rule about how PATCH requests should be handled or what the incoming data should look like. Rather than sending the actual domain data, a client could send a patch-specific description of the changes to be applied. Of course, the request handler would have to be written to handle patch instructions instead of the domain data.

In both @PutMapping and @PatchMapping, notice that the request path references the resource that’s to be changed. This is the same way paths are handled by @GetMapping-annotated methods.

You’ve now seen how to fetch and post resources with @GetMapping and @PostMapping. And you’ve seen two different ways of updating a resource with @PutMapping and @PatchMapping. All that’s left is handling requests to delete a resource.

6.1.4. Deleting data from the server

Sometimes data simply isn’t needed anymore. In those cases, a client should be able to request that a resource be removed with an HTTP DELETE request.

Spring MVC’s @DeleteMapping comes in handy for declaring methods that handle DELETE requests. For example, let’s say you want your API to allow for an order resource to be deleted. The following controller method should do the trick:

@DeleteMapping("/{orderId}")
@ResponseStatus(code=HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable("orderId") Long orderId) {
  try {
    repo.deleteById(orderId);
  } catch (EmptyResultDataAccessException e) {}
}

By this point, the idea of another mapping annotation should be old hat to you. You’ve already seen @GetMapping, @PostMapping, @PutMapping, and @PatchMapping—each specifying that a method should handle requests for their corresponding HTTP methods. It will probably come as no surprise to you that @DeleteMapping is used to specify that the deleteOrder() method is responsible for handling DELETE requests for /orders/{orderId}.

The code within the method is what does the actual work of deleting an order. In this case, it takes the order ID, provided as a path variable in the URL, and passes it to the repository’s deleteById() method. If the order exists when that method is called, it will be deleted. If the order doesn’t exist, an EmptyResultDataAccessException will be thrown.

I’ve chosen to catch the EmptyResultDataAccessException and do nothing with it. My thinking here is that if you try to delete a resource that doesn’t exist, the outcome is the same as if it did exist prior to deletion. That is, the resource will be nonexistent. Whether it existed before or not is irrelevant. Alternatively, I could’ve written deleteOrder() to return a ResponseEntity, setting the body to null and the HTTP status code to NOT FOUND.

The only other thing to take note of in the deleteOrder() method is that it’s annotated with @ResponseStatus to ensure that the response’s HTTP status is 204 (NO CONTENT). There’s no need to communicate any resource data back to the client for a resource that no longer exists, so responses to DELETE requests typically have no body and therefore should communicate an HTTP status code to let the client know not to expect any content.

Your Taco Cloud API is starting to take shape. The client-side code can now easily consume this API to present ingredients, accept orders, and display recently created tacos. But there’s something you can do that will make your API even easier for the client to consume. Let’s look at how you can add hypermedia to the Taco Cloud API.

6.2. Enabling hypermedia

The API you’ve created thus far is fairly basic, but it does work as long as the client that consumes it is aware of the API’s URL scheme. For example, a client may be hardcoded to know that it can obtain a list of recently created tacos by issuing a GET request for /design/recent. Likewise, it may be hardcoded to know that it can append the ID of any taco in that list to /design to get the URL for that particular taco resource.

Using hardcoded URL patterns and string manipulation is common among API client code. But imagine for a moment what would happen if the API’s URL scheme were to change. The hardcoded client code would have an obsolete understanding of the API and would thus be broken. Hardcoding API URLs and using string manipulation on them makes the client code brittle.

Hypermedia as the Engine of Application State, or HATEOAS, is a means of creating self-describing APIs wherein resources returned from an API contain links to related resources. 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 its understanding of those relationships to discover the API’s URLs as it traverses those relationships.

For example, suppose a client were to request a list of recently designed tacos. In its raw form, with no hyperlinks, the list of recent tacos would be received in the client with JSON that looks like this (with all but the first taco in the list clipped out for brevity’s sake):

[
  {
    "id": 4,
    "name": "Veg-Out",
    "createdAt": "2018-01-31T20:15:53.219+0000",
    "ingredients": [
      {"id": "FLTO", "name": "Flour Tortilla", "type": "WRAP"},
      {"id": "COTO", "name": "Corn Tortilla", "type": "WRAP"},
      {"id": "TMTO", "name": "Diced Tomatoes", "type": "VEGGIES"},
      {"id": "LETC", "name": "Lettuce", "type": "VEGGIES"},
      {"id": "SLSA", "name": "Salsa", "type": "SAUCE"}
    ]
  },
  ...
]

If the client wished to fetch or perform some other HTTP operation on the taco itself, it would need to know (via hardcoding) that it could append the value of the id property to a URL whose path is /design. Likewise, if it wanted to perform an HTTP operation on one of the ingredients, it would need to know that it could append the value of the ingredient’s id property to a URL whose path is /ingredients. In either case, it would also need to prefix that path with http:// or https:// and the hostname of the API.

In contrast, 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. The same list of recently created tacos might look like the next listing if hyperlinks were embedded.

Listing 6.3. A list of taco resources that includes hyperlinks
{
  "_embedded": {
    "tacoResourceList": [
      {
        "name": "Veg-Out",
        "createdAt": "2018-01-31T20:15:53.219+0000",
        "ingredients": [
          {
            "name": "Flour Tortilla", "type": "WRAP",
            "_links": {
              "self": { "href": "http://localhost:8080/ingredients/FLTO" }
            }
          },
          {
            "name": "Corn Tortilla", "type": "WRAP",
            "_links": {
              "self": { "href": "http://localhost:8080/ingredients/COTO" }
            }
          },
          {
            "name": "Diced Tomatoes", "type": "VEGGIES",
            "_links": {
              "self": { "href": "http://localhost:8080/ingredients/TMTO" }
            }
          },
          {
            "name": "Lettuce", "type": "VEGGIES",
            "_links": {
              "self": { "href": "http://localhost:8080/ingredients/LETC" }
            }
          },
          {
            "name": "Salsa", "type": "SAUCE",
            "_links": {
              "self": { "href": "http://localhost:8080/ingredients/SLSA" }
            }
          }
        ],
        "_links": {
          "self": { "href": "http://localhost:8080/design/4" }
        }
      },

      ...
    ]
  },
  "_links": {
    "recents": {
      "href": "http://localhost:8080/design/recent"
    }
  }
}

This particular flavor of HATEOAS is known as HAL (Hypertext Application Language; http://stateless.co/hal_specification.html), a simple and commonly used format for embedding hyperlinks in JSON responses.

Although this list isn’t as succinct as before, it does provide some useful information. Each element in this new list of tacos includes a property named _links that contains hyperlinks for the client to navigate the API. In this example, both tacos and ingredients each have self links to reference those resources, and the entire list has a recents link that references itself.

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 what the taco resource’s URL would look like. 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.

The Spring HATEOAS project brings hyperlink support to Spring. It offers a set of classes and resource assemblers that can be used to add links to resources before returning them from a Spring MVC controller.

To enable hypermedia in the Taco Cloud API, you’ll need to add the Spring HATEOAS starter dependency to the build:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

This starter not only adds Spring HATEOAS to the project’s classpath, but also provides for autoconfiguration to enable Spring HATEOAS. All you need to do is rework your controllers to return resource types instead of domain types.

You’ll start by adding hypermedia links to the list of recent tacos returned by a GET request to /design/recent.

6.2.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. Both types are capable of carrying links to other resources. When returned from a Spring MVC REST controller method, the links they carry will be included in the JSON (or XML) received by the client.

To add hyperlinks to the list of recently created tacos, you’ll need to revisit the recentTacos() method shown in listing 6.2. The original implementation returned a List<Taco>, which was fine at the time, but you’re going to need it to return a Resources object instead. The following listing shows a new implementation of recentTacos() that includes the first steps toward enabling hyperlinks in the recent tacos list.

Listing 6.4. Adding hyperlinks to resources
@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {
  PageRequest page = PageRequest.of(
          0, 12, Sort.by("createdAt").descending());

  List<Taco> tacos = tacoRepo.findAll(page).getContent();
  Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

  recentResources.add(
      new Link("http://localhost:8080/design/recent", "recents"));
  return recentResources;
}

In this new version of recentTacos(), you no longer return the list of tacos directly. Instead, you use Resources.wrap() to wrap the list of tacos as an instance of Resources<Resource<Taco>>, which is ultimately returned from the method. But before returning the Resources object, you add a link whose relationship name is recents and whose URL is http://localhost:8080/design/recent. As a consequence, the following snippet of JSON is included in the resource returned from the API request:

"_links": {
  "recents": {
    "href": "http://localhost:8080/design/recent"
  }
}

This is a good start, but you’ve still got some work to do. At this point, the only link you’ve added is to the entire list; no links are added to the taco resources themselves or to the ingredients of each taco. You’ll add those soon. But first, let’s address the hardcoded URL that you’ve given for the recents link.

Hardcoding a URL like this is a really bad idea. Unless your Taco Cloud ambitions are limited to only ever running the application on your own development machines, you need a way to not hardcode a URL with localhost:8080 in it. Fortunately, Spring HATEOAS provides help in the form of link builders.

The most useful of the Spring HATEOAS link builders is ControllerLinkBuilder. This link builder is smart enough to know what the hostname is without you having to hardcode it. And it provides a handy fluent API to help you build links relative to the base URL of any controller.

Using ControllerLinkBuilder, you can rewrite the hardcoded Link creation in recentTacos() with the following lines:

Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);
recentResources.add(
  ControllerLinkBuilder.linkTo(DesignTacoController.class)
                       .slash("recent")
                       .withRel("recents"));

Not only do you no longer need to hardcode the hostname, you also don’t have to specify the /design path. Instead, you 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 you’re creating.

What’s next is a call to one of my favorite methods in any Spring project: slash(). I love this method because it so succinctly describes exactly what it’s going to do. It quite literally appends a slash (/) and the given value to the URL. As a result, the URL’s path is /design/recent.

Finally, you specify a relation name for the Link. In this example, the relation is named recents.

Although I’m a big fan of the slash() method, ControllerLinkBuilder has another method that can help eliminate any hardcoding associated with link URLs. Instead of calling slash(), you 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. The following code uses the linkTo() method this way:

Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);
recentResources.add(
        linkTo(methodOn(DesignTacoController.class).recentTacos())
        .withRel("recents"));

Here I’ve decided to statically include the linkTo() and methodOn() methods (both from ControllerLinkBuilder) to keep the code easier to read. The methodOn() method takes the controller class and lets you make a call to the recentTacos() method, which is intercepted by ControllerLinkBuilder and used to determine not only the controller’s base path, but also the path mapped to recentTacos(). Now the entire URL is derived from the controller’s mappings, and absolutely no portion is hardcoded. Sweet!

6.2.2. Creating resource assemblers

Now you need to add links to the taco resource contained within the list. One option is to loop through each of the Resource<Taco> elements carried in the Resources object, adding a Link to each individually. But that’s a bit tedious and you’d need to repeat that looping code in the API wherever you return a list of taco resources.

We need a different tactic.

Rather than let Resources.wrap() create a Resource object for each taco in the list, you’re going to define a utility class that converts Taco objects to a new TacoResource object. The TacoResource object will look a lot like a Taco, but it will also be able to carry links. The next listing shows what a TacoResource might look like.

Listing 6.5. A taco resource carriying domain data and a list of hyperlinks
package tacos.web.api;
import java.util.Date;
import java.util.List;
import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Ingredient;
import tacos.Taco;

public class TacoResource extends ResourceSupport {

  @Getter
  private final String name;

  @Getter
  private final Date createdAt;

  @Getter
  private final List<Ingredient> ingredients;

  public TacoResource(Taco taco) {
    this.name = taco.getName();
    this.createdAt = taco.getCreatedAt();
    this.ingredients = taco.getIngredients();
  }

}

In a lot of ways, TacoResource isn’t that different from the Taco domain type. They both have name, createdAt, and ingredients properties. But TacoResource extends ResourceSupport to inherit a list of Link object and methods to manage the list of links.

What’s more, TacoResource doesn’t include the id property from Taco. That’s because there’s no need to expose any database-specific IDs in the API. The resource’s self link will serve as the identifier for the resource from the perspective of an API client.

Note

Domains and resources: separate or the same? Some Spring developers may choose to combine their domain and resource types into a single type by having their domain types extend ResourceSupport. There’s no right or wrong answer as to which is the correct way. I chose to create a separate resource type so that Taco isn’t unnecessarily cluttered with resource links for use cases where links aren’t needed. Also, by creating a separate resource type, I was able to easily leave the id property out so that it won’t be exposed in the API.

TacoResource has a single constructor that accepts a Taco and copies the pertinent properties from the Taco to its own properties. This makes it easy to convert a single Taco object to a TacoResource. But if you stop there, you’d still need looping to convert a list of Taco objects to a Resources<TacoResource>.

To aid in converting Taco objects to TacoResource objects, you’re also going to create a resource assembler. The following listing is what you’ll need.

Listing 6.6. A resource assembler that assembles taco resources
package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import tacos.Taco;

public class TacoResourceAssembler
       extends ResourceAssemblerSupport<Taco, TacoResource> {

  public TacoResourceAssembler() {
    super(DesignTacoController.class, TacoResource.class);
  }

  @Override
  protected TacoResource instantiateResource(Taco taco) {
    return new TacoResource(taco);
  }

  @Override
  public TacoResource toResource(Taco taco) {
    return createResourceWithId(taco.getId(), taco);
  }

}

TacoResourceAssembler has a default constructor that informs the superclass (ResourceAssemblerSupport) that it will be using DesignTacoController to determine the base path for any URLs in links it creates when creating a TacoResource.

The instantiateResource() method is overridden to instantiate a TacoResource given a Taco. This method would be optional if TacoResource had a default constructor. In this case, however, TacoResource requires construction with a Taco, so you’re required to override it.

Finally, the toResource() method is the only method that’s strictly mandatory when extending ResourceAssemblerSupport. Here you’re telling it to create a TacoResource object from a Taco, and to automatically give it a self link with the URL being derived from the Taco object’s id property.

On the surface, toResource() appears to have a similar purpose to instantiateResource(), but they serve slightly different purposes. Whereas instantiateResource() is intended to only instantiate a Resource object, toResource() is intended not only to create the Resource object, but also to populate it with links. Under the covers, toResource() will call instantiateResource().

Now tweak the recentTacos() method to make use of TacoResourceAssembler:

@GetMapping("/recent")
public Resources<TacoResource> recentTacos() {
  PageRequest page = PageRequest.of(
          0, 12, Sort.by("createdAt").descending());
  List<Taco> tacos = tacoRepo.findAll(page).getContent();

  List<TacoResource> tacoResources =
      new TacoResourceAssembler().toResources(tacos);
  Resources<TacoResource> recentResources =
      new Resources<TacoResource>(tacoResources);
  recentResources.add(
      linkTo(methodOn(DesignTacoController.class).recentTacos())
      .withRel("recents"));
  return recentResources;
}

Rather than return a Resources<Resource<Taco>>, recentTacos() now returns a Resources<TacoResource> to take advantage of your new TacoResource type. After fetching the tacos from the repository, you pass the list of Taco objects to the toResources() method on a TacoResourceAssembler. This handy method cycles through all of the Taco objects, calling the toResource() method that you overrode in TacoResourceAssembler to create a list of TacoResource objects.

With that TacoResource list, you next create a Resources<TacoResource> object and then populate it with the recents links as in the prior version of recentTacos().

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. To address that, you’ll create a new resource assembler for ingredients:

package tacos.web.api;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import tacos.Ingredient;

class IngredientResourceAssembler extends
          ResourceAssemblerSupport<Ingredient, IngredientResource> {

  public IngredientResourceAssembler() {
    super(IngredientController2.class, IngredientResource.class);
  }

  @Override
  public IngredientResource toResource(Ingredient ingredient) {
    return createResourceWithId(ingredient.getId(), ingredient);
  }

  @Override
  protected IngredientResource instantiateResource(
                                            Ingredient ingredient) {
    return new IngredientResource(ingredient);
  }

}

As you can see, IngredientResourceAssembler is much like TacoResourceAssembler, but it works with Ingredient and IngredientResource objects instead of Taco and TacoResource objects.

Speaking of IngredientResource, it looks like this:

package tacos.web.api;
import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Ingredient;
import tacos.Ingredient.Type;

public class IngredientResource extends ResourceSupport {

  @Getter
  private String name;

  @Getter
  private Type type;

  public IngredientResource(Ingredient ingredient) {
    this.name = ingredient.getName();
    this.type = ingredient.getType();
  }

}

As with TacoResource, IngredientResource extends ResourceSupport and copies pertinent properties from the domain type into its own set of properties (leaving out the id property).

All that’s left is to make a slight change to TacoResource so that it carries IngredientResource objects instead of Ingredient objects:

package tacos.web.api;
import java.util.Date;
import java.util.List;
import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Taco;

public class TacoResource extends ResourceSupport {

  private static final IngredientResourceAssembler
            ingredientAssembler = new IngredientResourceAssembler();

  @Getter
  private final String name;

  @Getter
  private final Date createdAt;

  @Getter
  private final List<IngredientResource> ingredients;

  public TacoResource(Taco taco) {
    this.name = taco.getName();
    this.createdAt = taco.getCreatedAt();
    this.ingredients =
        ingredientAssembler.toResources(taco.getIngredients());
  }

}

This new version of TacoResource creates a static final instance of IngredientResourceAssembler and uses its toResource() method to convert a given Taco object’s list of Ingredient into a list of IngredientResource.

Your recent tacos list is now completely outfitted with hyperlinks, not only for itself (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 listing 6.3.

You could stop here and move on to the next subject. But first I’ll address something that’s been bugging me about listing 6.3.

6.2.3. Naming embedded relationships

If you take a closer look at listing 6.3, you’ll notice that the top-level elements look like this:

{
  "_embedded": {
    "tacoResourceList": [
      ...
    ]
  }
}

Most notably, let me draw your attention to the name tacoResourceList under embedded. That name was derived from the fact that the Resources object was created from a List<TacoResource>. Not that it’s likely, but if you 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 any clients coded to count on that name.

The @Relation annotation can help break the coupling between the JSON field name and the resource type class names as defined in Java. By annotating TacoResource with @Relation, you can specify how Spring HATEOAS should name the field in the resulting JSON:

@Relation(value="taco", collectionRelation="tacos")
public class TacoResource extends ResourceSupport {
  ...
}

Here you’ve specified that when a list of TacoResource objects is used in a Resources object, it should be named tacos. And although you’re not making use of it in our API, a single TacoResource object should be referred to in JSON as taco.

As a result, the JSON returned from /design/recent will now look like this (no matter what refactoring you may or may not perform on TacoResource):

{
  "_embedded": {
    "tacos": [
      ...
    ]
  }
}

Spring HATEOAS makes adding links to your API rather straightforward and simple. Nonetheless, it did add several lines of code that you wouldn’t otherwise need. Because of that, some developers may choose to not bother with HATEOAS in their APIs, even if it means that the client code is subject to breakage if the API’s URL scheme changes. I encourage you to take HATEOAS seriously and not to take the lazy way out by not adding hyperlinks in your resources.

But if you insist on being lazy, then maybe there’s a win-win scenario for you if you’re using Spring Data for your repositories. Let’s see how Spring Data REST can help you automatically create APIs based on the data repositories you created with Spring Data in chapter 3.

6.3. Enabling data-backed services

As you saw in chapter 3, Spring Data performs a special kind of magic by automatically creating repository implementations based on interfaces you define in your code. But Spring Data has another trick up its sleeve that can help you define APIs for your application.

Spring Data REST is another member of the Spring Data family that automatically creates REST APIs for repositories created by Spring Data. By doing little more than adding Spring Data REST to your build, you get an API with operations for each repository interface you’ve defined.

To start using Spring Data REST, you add the following dependency to your build:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

Believe it or not, that’s all that’s required to expose a REST API in a project that’s already using Spring Data for automatic repositories. By simply having the Spring Data REST starter in the build, the application gets auto-configuration that enables automatic creation of a REST API for any repositories that were created by Spring Data (including Spring Data JPA, Spring Data Mongo, and so on).

The REST endpoints that Spring Data REST creates are at least as good as (and possibly even better than) the ones you’ve created yourself. So at this point, feel free to do a little demolition work and remove any @RestController-annotated classes you’ve created up to this point before moving on.

To try out the endpoints provided by Spring Data REST, you can fire up the application and start poking at some of the URLs. Based on the set of repositories you’ve already defined for Taco Cloud, you should be able to perform GET requests for tacos, ingredients, orders, and users.

For example, you can get a list of all ingredients by making a GET request for /ingredients. Using curl, you might get something that looks like this (abridged to only show the first ingredient):

$ curl localhost:8080/ingredients
{
  "_embedded" : {
    "ingredients" : [ {
      "name" : "Flour Tortilla",
      "type" : "WRAP",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/ingredients/FLTO"
        },
        "ingredient" : {
          "href" : "http://localhost:8080/ingredients/FLTO"
        }
      }
    },
    ...
    ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/ingredients"
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/ingredients"
    }
  }
}

Wow! By doing nothing more than adding a dependency to your build, you’re not only getting an endpoint for ingredients, but the resources that come back also contain hyperlinks! Pretending to be a client of this API, you can also use curl to follow the self link for the flour tortilla entry:

$ curl http://localhost:8080/ingredients/FLTO
{
  "name" : "Flour Tortilla",
  "type" : "WRAP",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/ingredients/FLTO"
    },
    "ingredient" : {
      "href" : "http://localhost:8080/ingredients/FLTO"
    }
  }
}

To avoid getting too distracted, we won’t waste much more time in this book digging into each and every endpoint and option that Spring Data REST has created. But you should know that it also supports POST, PUT, and DELETE methods for the endpoints it creates. That’s right: you can POST to /ingredients to create a new ingredient and DELETE /ingredients/FLTO to remove flour tortillas from the menu.

One thing you might want to do is set a base path for the API so that its endpoints are distinct and don’t collide with any controllers you write. (In fact, if you don’t remove the IngredientsController you created earlier, it will interfere with the /ingredients endpoint provided by Spring Data REST.) To adjust the base path for the API, set the spring.data.rest.base-path property:

spring:
  data:
    rest:
      base-path: /api

This sets the base path for Spring Data REST endpoints to /api. Consequently, the ingredients endpoint is now /api/ingredients. Now give this new base path a spin by requesting a list of tacos:

$ curl http://localhost:8080/api/tacos
{
  "timestamp": "2018-02-11T16:22:12.381+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/api/tacos"
}

Oh dear! That didn’t work quite as expected. You have an Ingredient entity and an IngredientRepository interface, which Spring Data REST exposed with an /api/ ingredients endpoint. So if you have a Taco entity and a TacoRepository interface, why doesn’t Spring Data REST give you an /api/tacos endpoint?

6.3.1. Adjusting resource paths and relation names

Actually, Spring Data REST does give you an endpoint for working with tacos. But as clever as Spring Data REST can be, it shows itself to be a tiny bit less awesome in how it exposes the tacos endpoint.

When creating endpoints for Spring Data repositories, Spring Data REST tries to pluralize the associated entity class. For the Ingredient entity, the endpoint is /ingredients. For the Order and User entities it’s /orders and /users. So far, so good.

But sometimes, such as with “taco”, it trips up on a word and the pluralized version isn’t quite right. As it turns out, Spring Data REST pluralized “taco” as “tacoes”, so to make a request for tacos, you must play along and request /api/tacoes:

% curl localhost:8080/api/tacoes
{
  "_embedded" : {
    "tacoes" : [ {
      "name" : "Carnivore",
      "createdAt" : "2018-02-11T17:01:32.999+0000",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/tacoes/2"
        },
        "taco" : {
          "href" : "http://localhost:8080/api/tacoes/2"
        },
        "ingredients" : {
          "href" : "http://localhost:8080/api/tacoes/2/ingredients"
        }
      }
    }]
  },
  "page" : {
    "size" : 20,
    "totalElements" : 3,
    "totalPages" : 1,
    "number" : 0
  }
}

You may be wondering how I knew that “taco” would be mispluralized as “tacoes”. As it turns out, Spring Data REST also exposes a home resource that has links for all exposed endpoints. Just make a GET request to the API base path to get the goods:

$ curl localhost:8080/api
{
  "_links" : {
    "orders" : {
      "href" : "http://localhost:8080/api/orders"
    },
    "ingredients" : {
      "href" : "http://localhost:8080/api/ingredients"
    },
    "tacoes" : {
      "href" : "http://localhost:8080/api/tacoes{?page,size,sort}",
      "templated" : true
    },
    "users" : {
      "href" : "http://localhost:8080/api/users"
    },
    "profile" : {
      "href" : "http://localhost:8080/api/profile"
    }
  }
}

As you can see, the home resource shows the links for all of your entities. Everything looks good, except for the tacoes link, where both the relation name and the URL have the odd pluralization of “taco”.

The good news is that you don’t have to accept this little quirk of Spring Data REST. By adding a simple annotation to the Taco class, you can tweak both the relation name and that path:

@Data
@Entity
@RestResource(rel="tacos", path="tacos")
public class Taco {
  ...
}

The @RestResource annotation lets you give the entity any relation name and path you want. In this case, you’re setting them both to “tacos”. Now when you request the home resource, you see the tacos link with correct pluralization:

"tacos" : {
  "href" : "http://localhost:8080/api/tacos{?page,size,sort}",
  "templated" : true
},

This also sorts out the path for the endpoint so that you can issue requests against /api/tacos to work with taco resources.

Speaking of sorting things out, let’s look at how you can sort the results from Spring Data REST endpoints.

6.3.2. Paging and sorting

You may have noticed that the links in the home resource all offer optional page, size, and sort parameters. By default, requests to a collection resource such as /api/tacos will return up to 20 items per page from the first page. But you can adjust the page size and the page displayed by specifying the page and size parameters in your request.

For example, to request the first page of tacos where the page size is 5, you can issue the following GET request (using curl):

$ curl "localhost:8080/api/tacos?size=5"

Assuming that there are more than five tacos to be seen, you can request the second page of tacos by adding the page parameter:

$ curl "localhost:8080/api/tacos?size=5&page=1"

Notice that the page parameter is zero-based, which means that asking for page 1 is actually asking for the second page. (You’ll also note that many command-line shells trip up over the ampersand in the request, which is why I quoted the whole URL in the preceding curl command.)

You could use string manipulation to add those parameters to the URL, but HATEOAS comes to the rescue by offering links for the first, last, next, and previous pages in the response:

"_links" : {
  "first" : {
    "href" : "http://localhost:8080/api/tacos?page=0&size=5"
  },
  "self" : {
    "href" : "http://localhost:8080/api/tacos"
  },
  "next" : {
    "href" : "http://localhost:8080/api/tacos?page=1&size=5"
  },
  "last" : {
    "href" : "http://localhost:8080/api/tacos?page=2&size=5"
  },
  "profile" : {
    "href" : "http://localhost:8080/api/profile/tacos"
  },
  "recents" : {
    "href" : "http://localhost:8080/api/tacos/recent"
  }
}

With these links, a client of the API need not keep track of what page it’s on and concatenate the parameters to the URL. Instead, it must simply know to look for one of these page navigation links by its name and follow it.

The sort parameter lets you sort the resulting list by any property of the entity. For example, you need a way to fetch the 12 most recently created tacos for the UI to display. You can do that by specifying the following mix of paging and sorting parameters:

$ curl "localhost:8080/api/tacos?sort=createdAt,desc&page=0&size=12"

Here the sort parameter specifies that you should sort by the createdDate property and that it should be sorted in descending order (so that the newest tacos are first). The page and size parameters specify that you should see the first page of 12 tacos.

This is precisely what the UI needs in order to show the most recently created tacos. It’s approximately the same as the /design/recent endpoint you defined in DesignTacoController earlier in this chapter.

There’s a small problem, though. The UI code will need to be hardcoded to request the list of tacos with those parameters. Sure, it will work. But you’re adding some brittleness to the client by making it too knowledgeable regarding how to construct an API request. It would be great if the client could look up the URL from a list of links. And it would be even more awesome if the URL were more succinct, like the /design/recent endpoint you had before.

6.3.3. Adding custom endpoints

Spring Data REST is great at creating endpoints for performing CRUD operations against Spring Data repositories. But sometimes you need to break away from the default CRUD API and create an endpoint that gets to the core of the problem.

There’s absolutely nothing stopping you from implementing any endpoint you want in a @RestController-annotated bean to supplement what Spring Data REST automatically generates. In fact, you could resurrect the DesignTacoController from earlier in the chapter, and it would still work alongside the endpoints provided by Spring Data REST.

But when you write your own API controllers, their endpoints seem somewhat detached from the Spring Data REST endpoints in a couple of ways:

  • Your own controller endpoints aren’t mapped under Spring Data REST’s base path. You could force their mappings to be prefixed with whatever base path you want, including the Spring Data REST base path, but if the base path were to change, you’d need to edit the controller’s mappings to match.
  • Any endpoints you define in your own controllers won’t be automatically included as hyperlinks in the resources returned by Spring Data REST endpoints. This means that clients won’t be able to discover your custom endpoints with a relation name.

Let’s address the concern about the base path first. Spring Data REST includes @RepositoryRestController, a new annotation for annotating controller classes whose mappings should assume a base path that’s the same as the one configured for Spring Data REST endpoints. Put simply, all mappings in a @RepositoryRestController-annotated controller will have their path prefixed with the value of the spring.data.rest.base-path property (which you’ve configured as /api).

Rather than resurrect the DesignTacoController, which had several handler methods you won’t need, you’ll create a new controller that only contains the recentTacos() method. RecentTacosController in the next listing is annotated with @RepositoryRestController to adopt Spring Data REST’s base path for its request mappings.

Listing 6.7. Applying Spring Data REST’s base path to a controller
package tacos.web.api;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import tacos.Taco;
import tacos.data.TacoRepository;

@RepositoryRestController
public class RecentTacosController {

  private TacoRepository tacoRepo;

  public RecentTacosController(TacoRepository tacoRepo) {
    this.tacoRepo = tacoRepo;
  }

  @GetMapping(path="/tacos/recent", produces="application/hal+json")
  public ResponseEntity<Resources<TacoResource>> recentTacos() {
    PageRequest page = PageRequest.of(
                          0, 12, Sort.by("createdAt").descending());
    List<Taco> tacos = tacoRepo.findAll(page).getContent();

    List<TacoResource> tacoResources =
        new TacoResourceAssembler().toResources(tacos);
    Resources<TacoResource> recentResources =
            new Resources<TacoResource>(tacoResources);
    recentResources.add(
        linkTo(methodOn(RecentTacosController.class).recentTacos())
            .withRel("recents"));
    return new ResponseEntity<>(recentResources, HttpStatus.OK);
  }

}

Even though @GetMapping is mapped to the path /tacos/recent, the @RepositoryRestController annotation at the class level will ensure that it will be prefixed with Spring Data REST’s base path. As you’ve configured it, the recentTacos() method will handle GET requests for /api/tacos/recent.

One important thing to notice is that although @RepositoryRestController is named similarly to @RestController, it doesn’t carry the same semantics as @RestController. Specifically, it doesn’t ensure that values returned from handler methods are automatically written to the body of the response. Therefore you need to either annotate the method with @ResponseBody or return a ResponseEntity that wraps the response data. Here you chose to return a ResponseEntity.

With RecentTacosController in play, requests for /api/tacos/recent will return up to 15 of the most recently created tacos, without the need for paging and sorting parameters in the URL. But it still doesn’t appear in the hyperlinks list when requesting /api/tacos. Let’s fix that.

6.3.4. Adding custom hyperlinks to Spring Data endpoints

If the recent tacos endpoint isn’t among the hyperlinks returned from /api/tacos, how will a client know how to fetch the most recent tacos? It’ll either have to guess or use the paging and sorting parameters. Either way, it’ll be hardcoded in the client code, which isn’t ideal.

By declaring a resource processor bean, however, you can add links to the list of links that Spring Data REST automatically includes. Spring Data HATEOAS offers ResourceProcessor, an interface for manipulating resources before they’re returned through the API. For your purposes, you need an implementation of ResourceProcessor that adds a recents link to any resource of type PagedResources<Resource <Taco>> (the type returned for the /api/tacos endpoint). The next listing shows a bean declaration method that defines such a ResourceProcessor.

Listing 6.8. Adding custom links to a Spring Data REST endpoint
@Bean
public ResourceProcessor<PagedResources<Resource<Taco>>>
  tacoProcessor(EntityLinks links) {

  return new ResourceProcessor<PagedResources<Resource<Taco>>>() {
    @Override
    public PagedResources<Resource<Taco>> process(
                        PagedResources<Resource<Taco>> resource) {
      resource.add(
          links.linkFor(Taco.class)
               .slash("recent")
               .withRel("recents"));
      return resource;
    }
  };
}

The ResourceProcessor shown in listing 6.8 is defined as an anonymous inner class and declared as a bean to be created in the Spring application context. Spring HATEOAS will discover this bean (as well as any other beans of type ResourceProcessor) automatically 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 for requests for /api/tacos.

Summary

  • REST endpoints can be created with Spring MVC, with controllers that follow the same programming model as browser-targeted controllers.
  • Controller handler methods can either be annotated with @ResponseBody or return ResponseEntity objects to bypass the model and view and write data directly to the response body.
  • The @RestController annotation simplifies REST controllers, eliminating the need to use @ResponseBody on handler methods.
  • Spring HATEOAS enables hyperlinking of resources returned from Spring MVC controllers.
  • Spring Data repositories can automatically be exposed as REST APIs using Spring Data REST.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset