Kotlin Spring Boot Tutorial Part 2: Creating REST endpoints for a task app

Habibi Coding | حبيبي كودنق
Nerd For Tech
Published in
7 min readJan 9, 2023
tutorial banner

Here is a quick summary of Part 1:

In the first part, we created the data layer. In the package data we have the class Task which is our entity and represents our table. Inside the data package, we have the model package which holds Priority, TaskDto and TaskRequest. Then we have the repository package where we defined an interface with methods to find a specific task, queries all open tasks and all closed tasks and one method to check if there is a duplicate task with a description. Here is a link to part 1.

Exception handling

After completing the repository we now can move to the actual logic, but first, define exceptions that could occur. Create a new package on the same level like data and repository and name it exception. Create the data class TaskNotFoundException.

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(HttpStatus.NOT_FOUND)
data class TaskNotFoundException(override val message: String) : RuntimeException()

As the name implies this class will be used if a task is not found. Annotate the data class with @ResponseStatus(HttpStatus.NOT_FOUND). This will later get us the 404 not found HTTP response code. Our data class needs to inherit from RuntimeException so it can throw an exception if something went wrong.

After that create a new data class inside the exception package. Name the new data class BadRequestException.

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(HttpStatus.BAD_REQUEST)
data class BadRequestException(override val message: String): RuntimeException()

This class will be used for a more general exception message as the name suggests when the client sends a bad request. Same drill as for TaskNotFoundException. But of course, the HTTP status message is different that’s why we need @ResponseStatus(HttpStatus.BAD_REQUEST). Our new data class BadRequestException also needs to inherit from RuntimeException to be able to throw exceptions.

The service class, time for actual logic — Yalla | يلا

It is time to create the package service on the same level as repository, data and exception. Inside the service package create the class TaskService.

@Service
class TaskService(private val repository: TaskRepository) {

private fun convertEntityToDto(task: Task): TaskDto {
return TaskDto(
task.id,
task.description,
task.isReminderSet,
task.isTaskOpen,
task.createdOn,
task.priority
)
}


private fun assignValuesToEntity(task: Task, taskRequest: TaskCreateRequest) {
task.description = taskRequest.description
task.isReminderSet = taskRequest.isReminderSet
task.isTaskOpen = taskRequest.isTaskOpen
task.createdOn = taskRequest.createdOn
task.priority = taskRequest.priority
}

private fun checkForTaskId(id: Long) {
if (!repository.existsById(id)) {
throw TaskNotFoundException("Task with ID: $id does not exist!")
}
}
}

For TaskService we want to use dependency injection provided by the Spring framework. The annotation `@Service` tells the Spring framework this class shall be used for dependency injection. We can also now use constructor injection and pass the Taskrepository. Next, we need some helper methods. Let us start with the method which should convert our entity to the data transfer object. Name it convertEntityToDto(task: Task) and pass a parameter of type Task. Pass the properties of the parameter object inside the constructor of TaskDto().

assignValuesToEntity(): This method will be used when the client sends a POST or PUT request. As the name tells us it will assign the values of the request class to the Task entity.

checkForTaskId(): This method checks if the requested task already exists. If not it throws TaskNotFoundException(). This method will be used for GET, DELETE, or PUT requests.

After the helper methods, create the methods to get a list of tasks.

fun getAllTasks(): List<TaskDto> =
repository.findAll().stream().map(this::convertEntityToDto).collect(Collectors.toList())

fun getAllOpenTasks(): List<TaskDto> =
repository.queryAllOpenTasks().stream().map(this::convertEntityToDto).collect(Collectors.toList())

fun getAllClosedTasks(): List<TaskDto> =
repository.queryAllClosedTasks().stream().map(this::convertEntityToDto).collect(Collectors.toList())

getAllTasks(): This method uses the findAll() method of the parent class JpaRepository and creates a stream of data then maps the entities to a list of DTO’s.

getAllOpenTasks() & getAllClosedTasks() both are using the methods we defined in the first part of the tutorial, where we are using the native SQL queries.

Now, just query a single task. For that, we need an id as a parameter. But first, we need to check if there is already a task with the parameter id in the database. If not TaskNotFoundException() gets thrown. Otherwise, fetch the task from the database and convert it to a dto.

fun getTaskById(id: Long): TaskDto {
checkForTaskId(id)
val task: Task = repository.findTaskById(id)
return convertEntityToDto(task)
}

For creating a task, here we need an actual request class so we take the TaskCreateRequest. Then we check if there is already a task with the created description. If yes then BadRequestException is thrown. Otherwise, the values from the request class will be added to the entity and saved in the database, and then returned as DTO.

fun createTask(createRequest: TaskCreateRequest): TaskDto {
if (repository.doesDescriptionExist(createRequest.description)) {
throw BadRequestException("There is already a task with description: ${createRequest.description}")
}
val task = Task()
assignValuesToEntity(task, createRequest)
val savedTask = repository.save(task)
return convertEntityToDto(savedTask)
}

To update a task here we need the TaskUpdateRequest data class. For updating a task we need to make sure the task already exists so call again the method checkForTaskId(). Then we save it to get the existing task from the repository. Next, we loop over all fields of the TaskUpdateRequest data class. If one property is null we just jump straight to the next. Otherwise, we use the ReflectionUtils class to update the value of the field of our existing task from the update request parameter. Lastly, we save our updated existing task again in the repository and display it to the client.

fun updateTask(id: Long, updateRequest: TaskUpdateRequest): TaskDto {
checkForTaskId(id)
val existingTask: Task = repository.findTaskById(id)

for (prop in TaskUpdateRequest::class.memberProperties) {
if (prop.get(updateRequest) != null) {
val field: Field? = ReflectionUtils.findField(Task::class.java, prop.name)
field?.let {
it.isAccessible = true
ReflectionUtils.setField(it, existingTask, prop.get(updateRequest))
}
}
}

val savedTask: Task = repository.save(existingTask)
return convertEntityToDto(savedTask)
}

At last, we add the logic for deleting a task. Here is the task id needed as a parameter. Check again if the ID is in the database, then go ahead and delete it from the database and return a success message to the client.

fun deleteTask(id: Long): String {
checkForTaskId(id)
repository.deleteById(id)
return "Task with id: $id has been deleted."
}

The Controller

Create a package controlleron the same level as service, repository, data and exception. Inside controller package create the class TaskController. Annotate it with `@RestController` so the Spring Framework knows this is our controller and handles dependency injection for us. In the next line add the annotation @RequestMapping("api") so our backend is available after the domain with /api. We also need to take advantage of an instance of TaskService so we use constructor injection here.

@RestController
@RequestMapping("api")
class TaskController(private val service: TaskService)

With an instance of TaskService in place. We can create our first GET endpoints. At the beginning let us fetch the three different lists of tasks we defined in TaskService. Fetch for getAllTasks(), getAllOpenTasks() and getAllClosedTasks(). The endpoint can be defined with the annotation @GetMapping("<your-endpoint>"). In order to give the client valid information wrap the response of the TaskService instance with ResponseEntity and add it inside HttpStatus.OK. If it fails before our classes in the package exception will take care of, which the logic was added in TaskService.

@GetMapping("all-tasks")
fun getAllTasks(): ResponseEntity<List<TaskDto>> = ResponseEntity(service.getAllTasks(), HttpStatus.OK)

@GetMapping("open-tasks")
fun getAllOpenTasks(): ResponseEntity<List<TaskDto>> = ResponseEntity(service.getAllOpenTasks(), HttpStatus.OK)

@GetMapping("closed-tasks")
fun getAllClosedTasks(): ResponseEntity<List<TaskDto>> = ResponseEntity(service.getAllClosedTasks(), HttpStatus.OK)

Add the endpoint GET /task/{id} the id is the path variable and needs to be passed as a parameter. Use the method of getTaskById(id) and wrap it again in ResponseEntity.

@GetMapping("task/{id}")
fun getTaskById(@PathVariable id: Long): ResponseEntity<TaskDto> =
ResponseEntity(service.getTaskById(id), HttpStatus.OK)

The next endpoint will be a POST endpoint /create the parameter is a TaskRequest. Annotate it with @Valid. We can put the @Valid annotation on method parameters and fields to tell Spring that we want a method parameter or field to be validated. There are three things we can validate for any incoming HTTP request:

  1. the request body
  2. variables within the path (e.g. id in /foos/{id})
  3. query parameters
@PostMapping("create")
fun createTask(
@Valid @RequestBody createRequest: TaskCreateRequest
): ResponseEntity<TaskDto> = ResponseEntity(service.createTask(createRequest), HttpStatus.OK)

We want to validate the request body that's why we also use the annotation @RequestBody for TaskCreateRequest. We let the service instance create a task entity with the request parameter then return a DTO wrapped in ResponseEntity.

Time to update a task with PATCH endpoint called /update/{id} same thing with the POST endpoint use the annotations @Valid and @RequestBody. It also takes a TaskUpdateRequest parameter. Then we let the service instance update a task entity with the request parameters id and updateRequest then return a DTO wrapped in ResponseEntity.

@PatchMapping("update/{id}")
fun updateTask(
@PathVariable id: Long,
@Valid @RequestBody updateRequest: TaskUpdateRequest
): ResponseEntity<TaskDto> = ResponseEntity(service.updateTask(id, updateRequest), HttpStatus.OK)

Finally, our last endpoint is a DELETE endpoint called delete/{id}. This one also takes the id as a path variable and gets wrapped in ResponseEntity like the ones before.

@DeleteMapping("delete/{id}")
fun deleteTask(@PathVariable id: Long): ResponseEntity<String> =
ResponseEntity(service.deleteTask(id), HttpStatus.OK)

Config for CORS errors

If you don’t what a Cross-Origin Resource Sharing error, I recommend reading this article by mozilla.org. After reading a little bit about it create a new package called config. Inside config create CorsConfig class. Annotate it with @Configuration. Then add the method getCorsConfiguration() and annotate this method with @Bean.

Spring @Configuration annotation is part of the spring core framework. Spring Configuration annotation indicates that the class has a@Bean definition method. The Spring container can process the class and generate Spring Beans to be used in the application.

@Configuration
class CorsConfig {

@Bean
fun getCorsConfiguration(): WebMvcConfigurer {
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
}
}
}
}

For simplicity purposes, we will only use wildcards for mapping, allowed origin, methods and header. Basically, any kind of request to our API will be allowed, in a real professional application, you should add more restrictions.

Okay, that’s it for the second part. If you enjoyed this article give it a clap. Here is Part 3.

Here is the completed project, check out branch part_two

By the way here is the link for the article as YouTube series: Kotlin Spring Boot Tutorial Series

--

--