Spring has long been one of the most powerful and widely used frameworks in the Java ecosystem. But as applications grow larger and teams prioritize readability, safety, and developer experience, Kotlin has become an increasingly popular choice for building Spring-based backends.

Kotlin brings:

In this post, we’ll build a CRUD To-Do List REST API using Spring 5 and Kotlin, covering everything from project setup to controllers, services, persistence, and best practices.

By the end, you’ll have a clean, idiomatic Kotlin API that you can easily extend into a real production service.


What We’re Building

We’ll create a RESTful API that supports:

Our stack:


Why Spring + Kotlin?

Before diving in, let’s talk about why this combination works so well.

Concise and expressive code

Kotlin dramatically reduces boilerplate compared to Java:

Null safety

Kotlin’s type system forces you to think about nullability at compile time, eliminating an entire class of runtime bugs.

Official Spring support

Spring 5 introduced first-class Kotlin support, including:


Step 1: Create the Project

The easiest way to start is Spring Initializr.

Choose:

Once generated, unzip and open the project in IntelliJ IDEA (recommended for Kotlin).


Step 2: Project Structure

A clean structure helps even small projects scale gracefully.

src/main/kotlin/com/example/todo
  ├── TodoApplication.kt
  ├── controller
  │   └── TodoController.kt
  ├── service
  │   └── TodoService.kt
  ├── repository
  │   └── TodoRepository.kt
  └── model
      └── Todo.kt

This separation keeps responsibilities clear:


Step 3: Define the To-Do Entity

Let’s start with the domain model.

Todo.kt

package com.example.todo.model

import javax.persistence.*

@Entity
@Table(name = "todos")
data class Todo(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    var title: String,

    @Column(nullable = false)
    var completed: Boolean = false
)

Why this works well in Kotlin


Step 4: Create the Repository

Spring Data JPA eliminates most persistence boilerplate.

TodoRepository.kt

package com.example.todo.repository

import com.example.todo.model.Todo
import org.springframework.data.jpa.repository.JpaRepository

interface TodoRepository : JpaRepository<Todo, Long>

That’s it.

You automatically get:

No implementation required.


Step 5: Create the Service Layer

The service layer contains business logic and protects your controllers from persistence details.

TodoService.kt

package com.example.todo.service

import com.example.todo.model.Todo
import com.example.todo.repository.TodoRepository
import org.springframework.stereotype.Service

@Service
class TodoService(
    private val todoRepository: TodoRepository
) {

    fun getAllTodos(): List<Todo> =
        todoRepository.findAll()

    fun getTodoById(id: Long): Todo =
        todoRepository.findById(id)
            .orElseThrow { RuntimeException("Todo not found") }

    fun createTodo(title: String): Todo =
        todoRepository.save(
            Todo(title = title)
        )

    fun updateTodo(id: Long, title: String, completed: Boolean): Todo {
        val todo = getTodoById(id)
        todo.title = title
        todo.completed = completed
        return todoRepository.save(todo)
    }

    fun deleteTodo(id: Long) =
        todoRepository.deleteById(id)
}

Why constructor injection shines in Kotlin


Step 6: Create Request DTOs

Separating request models from entities is a good habit.

CreateTodoRequest.kt

data class CreateTodoRequest(
    val title: String
)

UpdateTodoRequest.kt

data class UpdateTodoRequest(
    val title: String,
    val completed: Boolean
)

This prevents accidental over-posting and keeps your API contracts clear.


Step 7: Build the REST Controller

Now we expose our API.

TodoController.kt

package com.example.todo.controller

import com.example.todo.service.TodoService
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/todos")
class TodoController(
    private val todoService: TodoService
) {

    @GetMapping
    fun getAllTodos() =
        todoService.getAllTodos()

    @GetMapping("/{id}")
    fun getTodo(@PathVariable id: Long) =
        todoService.getTodoById(id)

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun createTodo(@RequestBody request: CreateTodoRequest) =
        todoService.createTodo(request.title)

    @PutMapping("/{id}")
    fun updateTodo(
        @PathVariable id: Long,
        @RequestBody request: UpdateTodoRequest
    ) =
        todoService.updateTodo(id, request.title, request.completed)

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    fun deleteTodo(@PathVariable id: Long) =
        todoService.deleteTodo(id)
}

Endpoints summary

MethodEndpointDescription
GET/api/todosGet all todos
GET/api/todos/{id}Get one todo
POST/api/todosCreate todo
PUT/api/todos/{id}Update todo
DELETE/api/todos/{id}Delete todo

Step 8: Configure H2 Database

Add to application.yml:

spring:
  datasource:
    url: jdbc:h2:mem:todo-db
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

Now you can access the H2 console at:

http://localhost:8080/h2-console

Step 9: Run and Test the API

Start the application:

./gradlew bootRun

Test with curl or Postman.

Create a todo

POST /api/todos
{
  "title": "Learn Spring with Kotlin"
}

Update a todo

PUT /api/todos/1
{
  "title": "Build Kotlin APIs",
  "completed": true
}

Common Kotlin + Spring Pitfalls

❌ Using val for mutable JPA fields

JPA needs mutable properties for updates. Use var.


❌ Forgetting default constructor values

Kotlin requires defaults for JPA proxying.


❌ Skipping DTOs

Direct entity binding leads to fragile APIs.


Why This Architecture Scales

Even though this is a simple CRUD API, the structure supports:

Spring + Kotlin excels when you keep layers clean and responsibilities well defined.


Final Thoughts

Building a CRUD To-Do List API with Spring 5 and Kotlin is an excellent way to experience how modern backend development should feel:

This setup isn’t just for demos — it’s the same foundation used in real-world Kotlin/Spring services running in production today.

If you want follow-up posts, I can walk through:

Just tell me what you want next by leaving me a comment.

Leave a Reply

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