CodeBlanks Logo Icon CodeBlanks

Putting it together

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

Main.kt
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/1

Sample solution

Main.kt
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

Question
val api = Api(repo)
println(api.____("/users/1"))
Answer
val api = Api(repo)
println(api.route("/users/1"))

Explanation: api.route interprets a path.

All lessons in Advanced