Lesson 4
Creating CRUD Endpoints with Spring Boot
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 the 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 of how to retrieve data. You've already created numerous GET endpoints in the previous exercises:

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

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:

Kotlin
1@PostMapping("/recipes") 2fun addRecipe(@RequestBody recipeItem: 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:

Kotlin
1@PutMapping("/recipes/{id}") 2fun updateRecipe(@PathVariable id: UUID, @RequestBody updatedRecipe: RecipeItem): RecipeItem { 3 val existingRecipe = recipeRepository.findById(id) ?: throw RuntimeException("Recipe not found!") 4 5 existingRecipe.title = updatedRecipe.title 6 existingRecipe.description = updatedRecipe.description 7 8 return recipeRepository.save(existingRecipe) 9}

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 Kotlin data class. In our Digital Recipe Book example, it will be a RecipeItem object. This process of converting a request body into a Kotlin object is called deserialization.

Deleting Data

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

Kotlin
1@DeleteMapping("/recipes/{id}") 2fun deleteRecipe(@PathVariable id: UUID): String { 3 val recipe = recipeRepository.findById(id) ?: throw RuntimeException("Recipe not found!") 4 5 recipeRepository.deleteById(id) 6 return "Recipe deleted!" 7}

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:

Kotlin
1@RestController 2@RequestMapping("/recipes") 3class RecipeController( 4 private val recipeRepository: RecipeRepository 5) { 6 7 @GetMapping 8 fun getAllRecipes(): List<RecipeItem> { 9 return recipeRepository.findAll() 10 } 11 12 @PostMapping 13 fun addRecipe(@RequestBody recipeItem: RecipeItem): RecipeItem { 14 recipeRepository.save(recipeItem) 15 return recipeItem 16 } 17 18 @GetMapping("/{id}") 19 fun getRecipeById(@PathVariable id: UUID): RecipeItem? { 20 return recipeRepository.findById(id) 21 } 22 23 @PutMapping("/{id}") 24 fun updateRecipe(@PathVariable id: UUID, @RequestBody updatedRecipe: RecipeItem): RecipeItem { 25 val existingRecipe = recipeRepository.findById(id) ?: throw RuntimeException("Recipe not found!") 26 27 existingRecipe.title = updatedRecipe.title 28 existingRecipe.description = updatedRecipe.description 29 30 return recipeRepository.save(existingRecipe) 31 } 32 33 @DeleteMapping("/{id}") 34 fun deleteRecipe(@PathVariable id: UUID): String { 35 val recipe = recipeRepository.findById(id) ?: throw RuntimeException("Recipe not found!") 36 37 recipeRepository.deleteById(id) 38 return "Recipe deleted!" 39 } 40}

By defining the /recipes prefix at the class level using @RequestMapping("/recipes"), you avoid repeating the prefix for each method. For instance, the method with @GetMapping("/{id}") is equivalent to @GetMapping("/recipes/{id}") when the @RequestMapping("/recipes") is used at the class level.

@RequestMapping on the Method Level

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

Kotlin
1@RequestMapping(value = ["/recipes"], method = [RequestMethod.GET]) 2fun getAllRecipes(): List<RecipeItem> { 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.