REST API with Spring Boot and MongoDB using Kotlin

If you think about Kotlin you could probably think about Android and mobile development.
It’s true. When Google announced Kotlin as the preferred language for the Android platform, its overall popularity increased sharply.

According to The State of Developer Ecosystem 2019, the survey made by JetBrains (Kotlin language creators), 62% of Kotlin-based apps were mobile. But 41% of Kotlin projects were related to web back-end development.

Indeed, Kotlin may be a good choice for server-side development. Among other frameworks, Kotlin is one of the officially supported languages of the Spring Framework.

In this text, I’d like to show you how easy it is to create a Spring application using Kotlin. I’m going to implement a simple CRUD REST API with Spring Boot and MongoDB as a database using Kotlin as a language.

So, get started with implementation and try it yourself.

Initialize the project

First of all, initialize the Spring Boot project. You can use the Spring Initializr tool.

Choose Gradle as a building tool and Kotlin as a language.

Then check Spring Web and Spring Data MongoDB as dependencies.

Generate, download, unzip and open the project in your IDE.

If you imported the project successfully you should be able to run the project.

2020-02-11 17:54:58.781  INFO 11364 --- [main] n.m.m.MongodbRestApiDemoApplicationKt    : Started MongodbRestApiDemoApplicationKt in 1.532 seconds (JVM running for 2.154)

MongoDB connection information

I assume you’ve installed and configured MongoDB on your local machine properly and we can easily go to connection configuration.

As I’m using default settings I have to specify basic data only in the application.properties – the host, port and database name. You don’t have to worry about database creation, it will be instantiated automatically.

spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mongo-rest-api-demo
application.properties

Create an entity class

When it comes to the domain, I’m not going to be very original here. Let’s say we would like to have a simple entity that will be a model of a task from a well-known to-do list. Let’s call it (surprise!) Task.

Let’s create an entity class file (Task.kt) in the domain package which will be used for further operations.

@Document
data class Task (
    @Id
    val id: ObjectId = ObjectId.get(),
    val title: String,
    val description: String,
    val createdDate: LocalDateTime = LocalDateTime.now(),
    val modifiedDate: LocalDateTime = LocalDateTime.now()
)
Task.kt

@Document annotation is used here for marking a class which objects we’d like to persist to the database. @Id is used for marking a field used for identification purposes. If you need to name your fields differently in the database document, you can use @Field(“fieldName”) annotation.

Apart from fields declaration, default values were provided for some of them as well. As simple as this.

Create repository

Now, let’s create a repository package with a repository interface file – TaskRepository.kt.

interface TaskRepository : MongoRepository<Task, String> {
    fun findOneById(id: ObjectId): Task
}
TaskRepository.kt

The repository is ready to use, we don’t have to write an implementation for it.

What you can see here is that only findOneById method was declared. It’s the feature called query methods and it’s provided by Spring Data JPA.
Additionally, MongoRepository interface provides all basic methods for CRUD operations which are inherited and will be used later.

Provide controller and prepare integration test

We can use our repository directly in the controller. For such simple operations, we don’t need an additional service layer.

Let’s create a new package called controller with TaskController.kt file inside it.

Now, inside of the file let’s create a class TaskController annotated with @RestController annotation and with TaskRepository injected via the constructor.

@RestController
@RequestMapping("/tasks")
class TaskController(
        private val taskRepository: TaskRepository
) {
    // TODO: Implement methods
}
TaskController.kt

In the meanwhile, let’s provide a test class for TestController. In src/test packages create controller package with TaskControllerIntegrationTest.kt file.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TaskControllerIntegrationTest @Autowired constructor(
        private val taskRepository: TaskRepository,
        private val restTemplate: TestRestTemplate
) {
    private val defaultTaskId = ObjectId.get()

    @LocalServerPort
    protected var port: Int = 0

    @BeforeEach
    fun setUp() {
        taskRepository.deleteAll()
    }


    private fun getRootUrl(): String? = "http://localhost:$port/tasks"

    private fun saveOneTask() = taskRepository.save(Task(defaultTaskId, "Title", "Description"))

}
TaskControllerIntegrationTest.kt

As you can see the test class is already prepared and has few helper methods in place.

SpringBootTest.WebEnvironment.RANDOM_PORT is used here.
It means that the server will be started on a random port that will be injected into the port field annotated with @LocalServerPort.

We’ll add some proper tests here a bit later. Let’s go back into the controller code.

GET – return all tasks and single task

First of all, let’s implement some basic retrieval methods.
One for returning all tasks from the database (GET /tasks) and another for returning a single task from the database by id (GET /tasks/{id}).
Both of them will return 200 (Success) code with proper entities.

    @GetMapping
    fun getAllTasks(): ResponseEntity<List<Task>> {
        val tasks = taskRepository.findAll()
        return ResponseEntity.ok(tasks)
    }

    @GetMapping("/{id}")
    fun getOneTask(@PathVariable("id") id: String): ResponseEntity<Task> {
        val task = taskRepository.findOneById(ObjectId(id))
        return ResponseEntity.ok(task)
    }
TaskController.kt

Looks nice, so let’s provide basic tests for these methods.

    @Test
    fun `should return all tasks`() {
        saveOneTask()

        val response = restTemplate.getForEntity(
                getRootUrl(),
                List::class.java
        )

        assertEquals(200, response.statusCode.value())
        assertNotNull(response.body)
        assertEquals(1, response.body?.size)
    }

    @Test
    fun `should return single task by id`() {
        saveOneTask()

        val response = restTemplate.getForEntity(
                getRootUrl() + "/$defaultTaskId",
                Task::class.java
        )

        assertEquals(200, response.statusCode.value())
        assertNotNull(response.body)
        assertEquals(defaultTaskId, response.body?.id)
    }
TaskControllerIntegrationTest.kt

I’m sure you get the given-when-then flow in these tests, they’re pretty straightforward.

Test naming convention

If you’re not familiar with Kotlin tests you may be surprised with the test naming used here (`should return all tasks`).
Well, it’s nothing more than a useful naming convention that allows us to write cleaner, more meaningful test methods names.
In JVM world similar conventions are well-known in Groovy and Scala communities.

Okay, so let’s run them. If it’s all-green, you’re ready to move into the next point.

POST – create a task

To create new tasks in a nice way let’s provide a request class, which will be used to pass title and description.

Create a new package in the domain called request and a new file called TaskRequest.kt. It will be useful for updating later on.

class TaskRequest(
        val title: String,
        val description: String
)
TaskRequest.kt

Then, move back into the controller file, let’s implement the proper creation method (POST /tasks). It will return a 201 (Created) code with newly created entity in the response body.

It should take TaskRequest object as a parameter in the request body and should save the newly created task into the database.

    @PostMapping
    fun createTask(@RequestBody request: TaskRequest): ResponseEntity<Task> {
        val task = taskRepository.save(Task(
                title = request.title,
                description = request.description
        ))
        return ResponseEntity(task, HttpStatus.CREATED)
    }
TaskController.kt

It’s time to test it, a new helper method (prepareTaskRequest) is provided for providing test TaskRequest object. It will be useful for tasks updating as well.

    @Test
    fun `should create new task`() {
        val taskRequest = prepareTaskRequest()

        val response = restTemplate.postForEntity(
                getRootUrl(),
                taskRequest,
                Task::class.java
        )


        assertEquals(201, response.statusCode.value())
        assertNotNull(response.body)
        assertNotNull(response.body?.id)
        assertEquals(taskRequest.description, response.body?.description)
        assertEquals(taskRequest.title, response.body?.title)
    }

    ...
    
    private fun prepareTaskRequest() = TaskRequest("Default title", "Default description")
TaskControllerIntegrationTest.kt

Great. Again, run it and check it if it passes.

PUT – update a task

Now, it’s time to provide an update method (PUT /tasks/{id}). According to HTTP reference, a successfully updated Task entity should be returned with 200 (Success) code.

    @PutMapping("/{id}")
    fun updateTask(@RequestBody request: TaskRequest, @PathVariable("id") id: String): ResponseEntity<Task> {
        val task = taskRepository.findOneById(ObjectId(id))
        val updatedTask = taskRepository.save(Task(
                id = task.id,
                title = request.title,
                description = request.description,
                createdDate = task.createdDate,
                modifiedDate = LocalDateTime.now()
        ))
        return ResponseEntity.ok(updatedTask)
    }
TaskController.kt

The test method will be very similar to the POST endpoint test.

    @Test
    fun `should update existing task`() {
        saveOneTask()
        val taskRequest = prepareTaskRequest()

        val updateResponse = restTemplate.exchange(
                getRootUrl() + "/$defaultTaskId",
                HttpMethod.PUT,
                HttpEntity(taskRequest, HttpHeaders()),
                Task::class.java
        )
        val updatedTask = taskRepository.findOneById(defaultTaskId)

        assertEquals(200, updateResponse.statusCode.value())
        assertEquals(defaultTaskId, updatedTask.id)
        assertEquals(taskRequest.description, updatedTask.description)
        assertEquals(taskRequest.title, updatedTask.title)
    }
TaskControllerIntegrationTest.kt

Run it and check if it passes successfully.

DELETE – delete a task

In order to remove tasks from the database, the delete method will be needed (DELETE /tasks/{id}).

As the deleted entity won’t be included in the response (obviously), the 204 (No content) code will be returned.

    @DeleteMapping("/{id}")
    fun deleteTask(@PathVariable("id") id: String): ResponseEntity<Unit> {
        taskRepository.deleteById(id)
        return ResponseEntity.noContent().build()
    }
TaskController.klt

The test method is pretty straightforward too.

    @Test
    fun `should delete existing task`() {
        saveOneTask()

        val delete = restTemplate.exchange(
                getRootUrl() + "/$defaultTaskId",
                HttpMethod.DELETE,
                HttpEntity(null, HttpHeaders()),
                ResponseEntity::class.java
        )

        assertEquals(204, delete.statusCode.value())
        assertThrows(EmptyResultDataAccessException::class.java) { taskRepository.findOneById(defaultTaskId) }
    }
TaskControllerIntegrationTest.kt

In this test, as in the previous ones, the returned status code value is checked. Also, assertThrows method is used to make an attempt to get a deleted entity from the database. This operation should throw EmptyResultDataAccessException.

Final results

The final shape of the TaskController should look like this:

@RestController
@RequestMapping("/tasks")
class TaskController(
        private val taskRepository: TaskRepository
) {

    @GetMapping
    fun getAllTasks(): ResponseEntity<List<Task>> {
        val tasks = taskRepository.findAll()
        return ResponseEntity.ok(tasks)
    }

    @GetMapping("/{id}")
    fun getOneTask(@PathVariable("id") id: String): ResponseEntity<Task> {
        val task = taskRepository.findOneById(ObjectId(id))
        return ResponseEntity.ok(task)
    }

    @PostMapping
    fun createTask(@RequestBody request: TaskRequest): ResponseEntity<Task> {
        val task = taskRepository.save(Task(
                title = request.title,
                description = request.description
        ))
        return ResponseEntity(task, HttpStatus.CREATED)
    }

    @PutMapping("/{id}")
    fun updateTask(@RequestBody request: TaskRequest, @PathVariable("id") id: String): ResponseEntity<Task> {
        val task = taskRepository.findOneById(ObjectId(id))
        val updatedTask = taskRepository.save(Task(
                id = task.id,
                title = request.title,
                description = request.description,
                createdDate = task.createdDate,
                modifiedDate = LocalDateTime.now()
        ))
        return ResponseEntity.ok(updatedTask)
    }

    @DeleteMapping("/{id}")
    fun deleteTask(@PathVariable("id") id: String): ResponseEntity<Unit> {
        taskRepository.deleteById(id)
        return ResponseEntity.noContent().build()
    }

}
TaskController.kt

And here’s the integration test class (TaskControllerIntegrationTest) for TaskController:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TaskControllerIntegrationTest @Autowired constructor(
        private val taskRepository: TaskRepository,
        private val restTemplate: TestRestTemplate
) {
    private val defaultTaskId = ObjectId.get()

    @LocalServerPort
    protected var port: Int = 0

    @BeforeEach
    fun setUp() {
        taskRepository.deleteAll()
    }

    @Test
    fun `should return all tasks`() {
        saveOneTask()

        val response = restTemplate.getForEntity(
                getRootUrl(),
                List::class.java
        )

        assertEquals(200, response.statusCode.value())
        assertNotNull(response.body)
        assertEquals(1, response.body?.size)
    }

    @Test
    fun `should return single task by id`() {
        saveOneTask()

        val response = restTemplate.getForEntity(
                getRootUrl() + "/$defaultTaskId",
                Task::class.java
        )

        assertEquals(200, response.statusCode.value())
        assertNotNull(response.body)
        assertEquals(defaultTaskId, response.body?.id)
    }

    @Test
    fun `should create new task`() {
        val taskRequest = prepareTaskRequest()

        val response = restTemplate.postForEntity(
                getRootUrl(),
                taskRequest,
                Task::class.java
        )

        assertEquals(201, response.statusCode.value())
        assertNotNull(response.body)
        assertNotNull(response.body?.id)
        assertEquals(taskRequest.description, response.body?.description)
        assertEquals(taskRequest.title, response.body?.title)
    }


    @Test
    fun `should update existing task`() {
        saveOneTask()
        val taskRequest = prepareTaskRequest()

        val updateResponse = restTemplate.exchange(
                getRootUrl() + "/$defaultTaskId",
                HttpMethod.PUT,
                HttpEntity(taskRequest, HttpHeaders()),
                Task::class.java
        )
        val updatedTask = taskRepository.findOneById(defaultTaskId)

        assertEquals(200, updateResponse.statusCode.value())
        assertEquals(defaultTaskId, updatedTask.id)
        assertEquals(taskRequest.description, updatedTask.description)
        assertEquals(taskRequest.title, updatedTask.title)
    }

    @Test
    fun `should delete existing task`() {
        saveOneTask()

        val delete = restTemplate.exchange(
                getRootUrl() + "/$defaultTaskId",
                HttpMethod.DELETE,
                HttpEntity(null, HttpHeaders()),
                ResponseEntity::class.java
        )

        assertEquals(204, delete.statusCode.value())
        assertThrows(EmptyResultDataAccessException::class.java) { taskRepository.findOneById(defaultTaskId) }
    }


    private fun getRootUrl(): String? = "http://localhost:$port/tasks"

    private fun saveOneTask() = taskRepository.save(Task(defaultTaskId, "Title", "Description"))

    private fun prepareTaskRequest() = TaskRequest("Default title", "Default description")

}
TaskControllerIntegrationTest.kt

The final results of the tests are very satisfying:

Further improvements

Controllers

As you can see, for the sake of brevity I decided to use a repository directly in controller’s methods, which is not always the recommended approach. In a real-world application, it’s recommended for business logic to be delegated into a separate service class.

Tests

In this example, I used TestRestTemplate for performing integration tests. It runs and hits the real server which is launched under the random port.
But it’s not the only approach. You can use MockMvc, which doesn’t require to run a servlet container.

Conclusions

As you can see, in comparison with Java, Kotlin allows you to write a much concise code that speeds things up and makes it easier to develop.

Not only the production code can be simplified but the tests as well, thanks to the conventions which allows us to name tests in a more readable way.

All in all, Kotlin can be a really good choice for a Spring-based project. And, what’s more important, it should not necessarily be a project rewritten from scratch or a greenfield. You can successfully integrate Kotlin with your existing Java codebase.

If you’re considering using Kotlin in your Spring Boot project I’d really recommend it. For me, Kotlin is a real game-changer for back-end development.

I’m curious if you ever had a chance to use Kotlin in your server-side project? Let me know in the comments about your experiences.

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *