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.
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 Method | Spring Annotation | Expected Operation |
---|---|---|
GET | @GetMapping | Read/Retrieve |
POST | @PostMapping | Create |
PUT | @PutMapping | Update/Replace |
DELETE | @DeleteMapping | Delete |
PATCH | @PatchMapping | Partial 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.
Let's begin with a quick recap of how to retrieve data. You've already created numerous GET
endpoints in the previous exercises:
Kotlin1@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.
Now that we've reviewed retrieval, let's move on to adding new data using a POST
endpoint:
Kotlin1@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.
Next, let's explore how to update existing data using a PUT
endpoint:
Kotlin1@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.
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:
JSON1{ 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.
Finally, let's discuss deleting data using a DELETE
endpoint:
Kotlin1@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.
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:
Kotlin1@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.
Before modern annotations like @GetMapping
and @PutMapping
emerged, @RequestMapping
was used to map various HTTP methods. For example:
Kotlin1@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.
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.