Lesson 4
Creating CRUD Endpoints
Introduction

Welcome back! So far, we’ve focused on retrieving data from our RESTful API using GET endpoints. Now, let’s take the next step: creating endpoints for creating, updating, and deleting data. Mastering these operations is critical for building fully functional APIs.

Understanding CRUD

In one of the previous lessons, we covered the concept of resources in REST. In RESTful APIs, a resource can be anything you manage, like a recipe. Each resource is identified by a URI, such as /recipes/123 or /users/john-doe. So far in this course, we have only focused on reading resources. What other operations can we perform on a resource? We can create, read, update, or delete a resource. These four operations form the abbreviation CRUD.

To perform these operations on a resource, different HTTP methods are used. Each method corresponds to one of the CRUD operations. Spring Boot allows for easy creation of endpoints for each method using special annotations. Here’s a quick overview of the HTTP methods, corresponding Spring annotations, and the expected operations:

HTTP MethodSpring AnnotationExpected Operation
GET@GetMappingRead/Retrieve
POST@PostMappingCreate
PUT@PutMappingUpdate/Replace
DELETE@DeleteMappingDelete
PATCH@PatchMappingPartial Update

Although PATCH is included here for completeness, we won’t be practicing it in this course since it’s less frequently used compared to the others.

Retrieving Data

Let's begin with a quick recap on how to retrieve data. You've already created numerous GET endpoints in the previous exercises:

Java
1@RestController 2@RequestMapping("/recipes") 3public class RecipeController { 4 5 @Autowired 6 private RecipeRepository recipeRepository; 7 8 @GetMapping 9 public List<RecipeItem> getAllRecipes() { 10 return recipeRepository.findAll(); 11 } 12 13 @GetMapping("/{id}") 14 public RecipeItem getRecipeById(@PathVariable UUID id) { 15 return recipeRepository.findById(id); 16 } 17}

The first method getAllRecipes returns a list of all recipes when a GET request is made to /recipes. The second method getRecipeById returns a single recipe based on the provided ID when a GET request is made to /recipes/{id}. The @GetMapping annotation maps the HTTP GET requests to these methods.

Creating Data

Now that we've reviewed retrieval, let's move on to adding new data using a POST endpoint:

Java
1@PostMapping("/recipes") 2public RecipeItem addRecipe(@RequestBody RecipeItem recipeItem) { 3 recipeRepository.save(recipeItem); 4 return recipeItem; 5}

The @PostMapping annotation maps HTTP POST requests to the addRecipe method. The @RequestBody annotation binds the recipe data from the request body to a RecipeItem object, which is then saved to the repository.

Updating Data

Next, let's explore how to update existing data using a PUT endpoint:

Java
1@PutMapping("/recipes/{id}") 2public RecipeItem updateRecipe(@PathVariable UUID id, @RequestBody RecipeItem updatedRecipe) { 3 RecipeItem existingRecipe = recipeRepository.findById(id); 4 if (existingRecipe == null) { 5 throw new RuntimeException("Recipe not found!"); 6 } 7 8 existingRecipe.setTitle(updatedRecipe.getTitle()); 9 existingRecipe.setDescription(updatedRecipe.getDescription()); 10 11 return existingRecipe; 12}

The @PutMapping annotation maps HTTP PUT requests to the updateRecipe method. The @PathVariable annotation extracts the recipe ID from the URI, while the @RequestBody annotation binds the updated recipe details from the request body.

Understanding Request Body

In the context of a POST or PUT request, the request body is typically a JSON object containing the data you wish to create or update. For instance, when updating a recipe, your request body might look like this:

JSON
1{ 2 "title": "Updated Recipe Title", 3 "description": "Updated Recipe Description" 4}

Using the @RequestBody annotation, this JSON object is automatically converted into a corresponding POJO (Plain Old Java Object). In our Digital Recipe Book example, it will be a RecipeItem object. This process of converting a request body into a POJO is called deserialization.

Deleting Data

Finally, let's discuss deleting data using a DELETE endpoint:

Java
1@DeleteMapping("/recipes/{id}") 2public String deleteRecipe(@PathVariable UUID id) { 3 RecipeItem recipe = recipeRepository.findById(id); 4 if (recipe == null) { 5 throw new RuntimeException("Recipe not found!"); 6 } 7 8 recipeRepository.delete(id); 9 return "Recipe deleted!"; 10}

The @DeleteMapping annotation maps HTTP DELETE requests to the deleteRecipe method. The @PathVariable annotation helps to extract the recipe ID from the URI in order to delete the specific recipe.

Setting Common URL Prefix using @RequestMapping

Let's take a closer look at how common URL prefixes can simplify our code. Right now, our RecipeController has four endpoints with the /recipes prefix:

Java
1@RestController 2@RequestMapping("/recipes") 3public class RecipeController { 4 5 @Autowired 6 private RecipeRepository recipeRepository; 7 8 @GetMapping 9 public List<RecipeItem> getAllRecipes() { 10 return recipeRepository.findAll(); 11 } 12 13 @PostMapping 14 public RecipeItem addRecipe(@RequestBody RecipeItem recipeItem) { 15 recipeRepository.save(recipeItem); 16 return recipeItem; 17 } 18 19 @GetMapping("/{id}") 20 public RecipeItem getRecipeById(@PathVariable UUID id) { 21 return recipeRepository.findById(id); 22 } 23 24 @PutMapping("/{id}") 25 public RecipeItem updateRecipe(@PathVariable UUID id, @RequestBody RecipeItem updatedRecipe) { 26 RecipeItem existingRecipe = recipeRepository.findById(id); 27 if (existingRecipe == null) { 28 throw new RuntimeException("Recipe not found!"); 29 } 30 31 existingRecipe.setTitle(updatedRecipe.getTitle()); 32 existingRecipe.setDescription(updatedRecipe.getDescription()); 33 34 return existingRecipe; 35 } 36 37 @DeleteMapping("/{id}") 38 public String deleteRecipe(@PathVariable UUID id) { 39 RecipeItem recipe = recipeRepository.findById(id); 40 if (recipe == null) { 41 throw new RuntimeException("Recipe not found!"); 42 } 43 44 recipeRepository.delete(id); 45 return "Recipe deleted!"; 46 } 47}

By defining the /recipes prefix at the class level using @RequestMapping("/recipes"), you avoid repeating the prefix for each method.

@RequestMapping on the Method Level

Before modern annotations like @GetMapping and @PutMapping emerged, @RequestMapping was used to map various HTTP methods. For example:

Java
1@RequestMapping(value = "/recipes", method = RequestMethod.GET) 2public List<RecipeItem> getAllRecipes() { 3 return recipeRepository.findAll(); 4}

While we mostly use the more specific annotations nowadays, this approach can still be useful for handling complex mapping scenarios, such as mapping multiple HTTP methods to a single endpoint, but that’s beyond the scope of our lesson.

Summary

In this lesson, you’ve learned how to create CRUD endpoints using Spring Boot. We covered how to retrieve, create, update, and delete data in a RESTful API, as well as how to streamline our URL mappings with @RequestMapping. This foundational knowledge will enable you to build robust and fully functional APIs. In the upcoming practice exercises, you’ll have the opportunity to implement these CRUD operations yourself, reinforcing what you've learned in this lesson.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.