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:
- Null safety by default
- Concise syntax
- Excellent interoperability with Java
- First-class support in Spring 5+
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:
- Create a to-do item
- Read all to-do items
- Read a single to-do item
- Update a to-do item
- Delete a to-do item
Our stack:
- Spring 5
- Spring Boot
- Kotlin
- Spring Data JPA
- H2 (in-memory DB for simplicity)
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:
- No getters/setters
- Data classes instead of POJOs
- Constructor injection without ceremony
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:
- Kotlin-friendly APIs
- Nullability annotations
- Improved DSLs
Step 1: Create the Project
The easiest way to start is Spring Initializr.
Choose:
- Project: Gradle or Maven
- Language: Kotlin
- Spring Boot: 2.x (Spring 5 under the hood)
- Dependencies:
- Spring Web
- Spring Data JPA
- H2 Database
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:
- Controller → HTTP layer
- Service → business logic
- Repository → persistence
- Model → domain entities
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
data classgives usequals,hashCode, andtoString- Default values make object creation simple
- Mutable fields (
var) allow updates - No boilerplate annotations beyond JPA essentials
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:
findAllfindByIdsavedeleteById
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
- No
@Autowired - Immutable dependencies
- Clean, testable code
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
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/todos | Get all todos |
| GET | /api/todos/{id} | Get one todo |
| POST | /api/todos | Create 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:
- Authentication layers
- Validation
- Pagination
- DTO mapping
- Event publishing
- Microservice extraction
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:
- Less boilerplate
- Strong typing
- Clear architecture
- Production-ready patterns
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:
- Adding validation with
@Valid - Pagination and sorting
- JWT authentication
- PostgreSQL instead of H2
- Writing integration tests in Kotlin
- Migrating a Java Spring app to Kotlin
Just tell me what you want next by leaving me a comment.