Putting it together
- Kotlin course
- Advanced
- Lesson 83/83
- 6 min
Combine all layers into a working mini service.
Bringing it together: API + DB + coroutines
The final lesson combines all pieces. UserRepo manages data with suspend methods, UsersTable stores data, Api routes with suspend route, main is called with runBlocking.
This structure brings together the Ktor API + Exposed DB + coroutines pattern at a small scale. You can now add a user to the repo and access it via the suspend API. This pattern is the foundation of production Kotlin backends.
Project structure
The final project has four layers. User data class at the bottom; UsersTable stores it; UserRepo wraps business logic; Api routes incoming paths to the repo.
All suspend methods are called inside runBlocking. This separation lets you test each layer independently and migrate to a real DB later. The fifth lesson shows this flow working with a single main call.
Practice Final: combine all pieces
Keep all existing definitions. In main add a user with repo.save(User(1, "Alex")), create val api = Api(repo) and print println(api.route("/users/1")). main must open with runBlocking.
Starter code
import kotlinx.coroutines.runBlocking
data class User(val id: Int, val name: String)
object UsersTable {
val rows = mutableListOf<User>()
fun insert(u: User) { rows.add(u) }
fun select(id: Int): User? = rows.find { it.id == id }
}
class UserRepo {
suspend fun save(u: User) { UsersTable.insert(u) }
suspend fun find(id: Int): User? = UsersTable.select(id)
}
class Api(val repo: UserRepo) {
suspend fun route(path: String): String {
return when {
path.startsWith("/users/") -> {
val id = path.removePrefix("/users/").toInt()
repo.find(id)?.name ?: "not found"
}
else -> "404"
}
}
}
// main = runBlocking: add User, create Api, print /users/1Sample solution
import kotlinx.coroutines.runBlocking
data class User(val id: Int, val name: String)
object UsersTable {
val rows = mutableListOf<User>()
fun insert(u: User) { rows.add(u) }
fun select(id: Int): User? = rows.find { it.id == id }
}
class UserRepo {
suspend fun save(u: User) { UsersTable.insert(u) }
suspend fun find(id: Int): User? = UsersTable.select(id)
}
class Api(val repo: UserRepo) {
suspend fun route(path: String): String {
return when {
path.startsWith("/users/") -> {
val id = path.removePrefix("/users/").toInt()
repo.find(id)?.name ?: "not found"
}
else -> "404"
}
}
}
fun main() = runBlocking {
val repo = UserRepo()
repo.save(User(1, "Alex"))
val api = Api(repo)
println(api.route("/users/1"))
}Expected output
Alex
Questions and answers
Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.
1 Why does main open with runBlocking in this project?
- Because it is faster
- Because suspend functions are called Correct answer
- Because tests are mandatory
- Because it gives a compile error
Explanation: runBlocking opens a scope for suspend functions.
2 Match the project parts
| Term | Meaning |
|---|---|
UserRepo |
Business logic |
UsersTable |
Data layer |
Api |
Routing |
runBlocking |
Coroutine scope |
3 Complete the route call
val api = Api(repo)
println(api.____("/users/1"))
val api = Api(repo)
println(api.route("/users/1"))
Explanation: api.route interprets a path.