7.1.1 Retrieving data from the server
One thing that we’d like the Taco Cloud application to be able to do is allow taco fanatics to design their own taco creations and share them with their follow taco lovers. One way to do that is to display a list of the most recently created tacos on the website.
In support of that feature, we need to create an endpoint that handles GET requests for /api/tacos which include a “recent” parameter 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 7.1 A RESTful controller for taco design API requests
package tacos.web.api;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tacos.Taco;
import tacos.data.TacoRepository;
@RestController
@RequestMapping(path="/api/tacos",
produces="application/json")
@CrossOrigin(origins="*")
public class TacoController {
private TacoRepository tacoRepo;
public TacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
@GetMapping(params="recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
}You may be thinking that this controller’s name sounds somewhat familiar. In chapter 2 you created a similarly named DesignTacoController that handled similar types of requests. But where that controller was for producing an HTML result in the Taco Cloud application, this new TacoController 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 TacoController with @Controller, just like 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.
You’ll notice that the @RequestMapping annotation also sets a produces attribute. This specifies that any of the handler methods in TacoController will handle requests only if the client sends a request with an Accept header that includes "application/json", indicating that the client can handle responses only in JSON format. This use of produces limits your API to only producing JSON results, and it allows for another controller (perhaps the TacoController from chapter 2) to handle requests with the same paths, so long as those requests don’t require JSON output.
Even though setting produces to "application/json" 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/xml" to the produces attribute as follows:
@RequestMapping(path="/api/tacos",
produces={"application/json", "text/xml"})The other thing you may have noticed in listing 7.1 is that the class is annotated with @CrossOrigin. It’s common for a JavaScript-based user interface, such as those written in a framework like Angular or ReactJS, to be served from a separate host and/or port from the API (at least for now), and the web browser will prevent your 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 localhost, port 8080, to access the API. The origins attribute accepts an array, however, so you can also specify multiple values, as shown next:
@RestController
@RequestMapping(path="/api/tacos",
produces="application/json")
@CrossOrigin(origins={"http://tacocloud:8080", "http://tacocloud.com"})
public class TacoController {
...
}The logic within the recentTacos() method is fairly straightforward. It constructs a PageRequest object that specifies that you want only 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 7.1, will be used as model data to display to the user).
You now have the start of a Taco Cloud API for your client. 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/api/tacos?recentOr like this, if you prefer HTTPie:
$ http :8080/api/tacos?recentInitially, the database will be empty, so the results from these requests will likewise be empty. We’ll see in a moment how to handle POST requests that save tacos. But in the meantime, you could add an CommandLineRunner bean to preload the database with some test data. The following CommandLineRunner bean method shows how you might preload a few ingredients and a few tacos:
@Bean
public CommandLineRunner dataLoader(
IngredientRepository repo,
UserRepository userRepo,
PasswordEncoder encoder,
TacoRepository tacoRepo) {
return args -> {
Ingredient flourTortilla = new Ingredient(
"FLTO", "Flour Tortilla", Type.WRAP);
Ingredient cornTortilla = new Ingredient(
"COTO", "Corn Tortilla", Type.WRAP);
Ingredient groundBeef = new Ingredient(
"GRBF", "Ground Beef", Type.PROTEIN);
Ingredient carnitas = new Ingredient(
"CARN", "Carnitas", Type.PROTEIN);
Ingredient tomatoes = new Ingredient(
"TMTO", "Diced Tomatoes", Type.VEGGIES);
Ingredient lettuce = new Ingredient(
"LETC", "Lettuce", Type.VEGGIES);
Ingredient cheddar = new Ingredient(
"CHED", "Cheddar", Type.CHEESE);
Ingredient jack = new Ingredient(
"JACK", "Monterrey Jack", Type.CHEESE);
Ingredient salsa = new Ingredient(
"SLSA", "Salsa", Type.SAUCE);
Ingredient sourCream = new Ingredient(
"SRCR", "Sour Cream", Type.SAUCE);
repo.save(flourTortilla);
repo.save(cornTortilla);
repo.save(groundBeef);
repo.save(carnitas);
repo.save(tomatoes);
repo.save(lettuce);
repo.save(cheddar);
repo.save(jack);
repo.save(salsa);
repo.save(sourCream);
Taco taco1 = new Taco();
taco1.setName("Carnivore");
taco1.setIngredients(Arrays.asList(
flourTortilla, groundBeef, carnitas,
sourCream, salsa, cheddar));
tacoRepo.save(taco1);
Taco taco2 = new Taco();
taco2.setName("Bovine Bounty");
taco2.setIngredients(Arrays.asList(
cornTortilla, groundBeef, cheddar,
jack, sourCream));
tacoRepo.save(taco2);
Taco taco3 = new Taco();
taco3.setName("Veg-Out");
taco3.setIngredients(Arrays.asList(
flourTortilla, cornTortilla, tomatoes,
lettuce, salsa));
tacoRepo.save(taco3);
};
}Now if you try to use curl or HTTPie to make a request to the recent tacos endpoint, you’ll get a response something like this (response formatted for readability):
$ curl localhost:8080/api/tacos?recent
[
{
"id": 4,
"name": "Veg-Out",
"createdAt": "2021-08-02T00:47:09.624+00:00",
"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" }
]
},
{
"id": 3,
"name": "Bovine Bounty",
"createdAt": "2021-08-02T00:47:09.621+00:00",
"ingredients": [
{ "id": "COTO", "name": "Corn Tortilla", "type": "WRAP" },
{ "id": "GRBF", "name": "Ground Beef", "type": "PROTEIN" },
{ "id": "CHED", "name": "Cheddar", "type": "CHEESE" },
{ "id": "JACK", "name": "Monterrey Jack", "type": "CHEESE" },
{ "id": "SRCR", "name": "Sour Cream", "type": "SAUCE" }
]
},
{
"id": 2,
"name": "Carnivore",
"createdAt": "2021-08-02T00:47:09.520+00:00",
"ingredients": [
{ "id": "FLTO", "name": "Flour Tortilla", "type": "WRAP" },
{ "id": "GRBF", "name": "Ground Beef", "type": "PROTEIN" },
{ "id": "CARN", "name": "Carnitas", "type": "PROTEIN" },
{ "id": "SRCR", "name": "Sour Cream", "type": "SAUCE" },
{ "id": "SLSA", "name": "Salsa", "type": "SAUCE" },
{ "id": "CHED", "name": "Cheddar", "type": "CHEESE" }
]
}
]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 as follows:
@GetMapping("/{id}")
public Optional<Taco> tacoById(@PathVariable("id") Long id) {
return tacoRepo.findById(id);
}Because the controller’s base path is /api/tacos, this controller method handles GET requests for /api/tacos/{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. The repository’s findById() method returns an Optional <Taco>, because it is possible that there may not be a taco that matches the given ID. The Optional<Taco> is simply returned from the controller method.
Spring then takes the Optional<Taco> and calls its get() method to produce the response. If the ID doesn’t match any known tacos, the response body will contain “null” and the response’s HTTP status code will be 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, as shown here:
@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.
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.
