Ktor for Spring Boot Developers

Spread the love

Overview

Ktor is an open source framework  built by JetBrains for building asynchronous servers and clients in Kotlin.

Ktor offers:

    • Lightweight, minimal, fast startup
    • Kotlin -first and coroutines
    • Interoperable with Java
    • Tooling support- tight integration with Intellij IDEA
    • Fast Growing ecosystem
    • Supports Microservices, APIs, serverless
    • Cleaner Kotlin DSLs

Introduction

In this article, we will show you how to create  a Ktor based rest application. For this, we will use order management application created using Spring Boot in one of my previous post https://medium.com/ranjeshblogs/how-to-create-restful-api-with-spring-boot-2-1-fee9f477e8a7

Application Startup

The following shows entry point of Ktor application and its conceptually similar to Spring Boot application’s @SpringBootApplication annotated class and its main method.

fun main() {
   embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
fun Application.module() {
   install(Koin) {
       modules(appModule)
   }
   DatabaseFactory.init()
   install(ContentNegotiation) {
       json(Json {
           prettyPrint = true
           isLenient = true
           ignoreUnknownKeys = true
       })
   }
   routing {
       userRoutes()
       orderRoutes()
   }
}

Routes

Routes in Ktor is like Controller in Spring Boot

UserRoutes

fun Route.userRoutes() {
   val userService by inject<UserService>()
   route("/api/users") {
       get {
           call.respond(userService.getAllUsers())
       }
       get("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid or missing ID")
               return@get
           }
           val user = userService.getUser(id)
           if (user == null) {
               call.respond(HttpStatusCode.NotFound, "User not found")
           } else {
               call.respond(user)
           }
       }
       post {
           val user = call.receive<User>()
           val savedUser = userService.createUser(user)
           call.respond(HttpStatusCode.Created, savedUser)
       }
       put("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid or missing ID")
               return@put
           }
           val user = call.receive<User>()
           val updatedUser = userService.updateUser(id, user)
           if (updatedUser == null) {
               call.respond(HttpStatusCode.NotFound, "User not found")
           } else {
               call.respond(updatedUser)
           }
       }
       patch("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid ID")
               return@patch
           }
           val updates = call.receive<PartialUserUpdate>()
           val updated = userService.patchUser(id, updates)
           if (updated == null) {
               call.respond(HttpStatusCode.NotFound, "User not found")
           } else {
               call.respond(updated)
           }
       }
       delete("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid or missing ID")
               return@delete
           }
           val deleted = userService.deleteUser(id)
           if (deleted) {
               call.respondText("User deleted successfully.")
           } else {
               call.respond(HttpStatusCode.NotFound, "User not found")
           }
       }
   }
}

Tables

Users

Following is equivalent of Spring @Entity in Ktor:

object Users : LongIdTable("users") {
   val firstName = varchar("first_name", length = 50)
   val lastName = varchar("last_name", length = 50)
   val email = varchar("email", length = 120)
   val firstLineOfAddress = varchar("first_line_of_address", length = 50)
   val secondLineOfAddress = varchar("second_line_of_address", length = 50).nullable()
   val town = varchar("town", length = 50)
   val postCode = varchar("post_code", length = 10)
}

DAO

UserDAO

Following is equivalent of Spring Repository layer in Ktor:

class UserDAO {
   fun getAll(): List<User> = transaction {
       Users.selectAll().map { toUser(it) }
   }

   fun getById(id: Long): User? = transaction {
       Users.select { Users.id eq id }.map { toUser(it) }.singleOrNull()
   }

   fun delete(id: Long): Boolean = transaction {
       Users.deleteWhere { Users.id eq id } > 0
   }

   fun update(id: Long, user: User): User? = transaction {
       val updatedRows = Users.update({ Users.id eq id }) {
           it[firstName] = user.firstName
           it[lastName] = user.lastName
           it[email] = user.email
           it[firstLineOfAddress] = user.firstLineOfAddress
           it[secondLineOfAddress] = user.secondLineOfAddress
           it[town] = user.town
           it[postCode] = user.postCode
       }
       if (updatedRows > 0) getById(id) else null
   }

   fun add(user: User): User = transaction {
       val id = Users.insert {
           it[firstName] = user.firstName
           it[lastName] = user.lastName
           it[email] = user.email
           it[firstLineOfAddress] = user.firstLineOfAddress
           it[secondLineOfAddress] = user.secondLineOfAddress
           it[town] = user.town
           it[postCode] = user.postCode
       } get Users.id
       user.copy(id = id.value)
   }

   fun patch(id: Long, updates: PartialUserUpdate): User? = transaction {
       Users.update({ Users.id eq id }) {
           updates.firstName?.let { f -> it[firstName] = f }
           updates.lastName?.let { l -> it[lastName] = l }
           updates.email?.let { e -> it[email] = e }
           updates.firstLineOfAddress?.let { f -> it[firstLineOfAddress] = f }
           updates.secondLineOfAddress?.let { s -> it[secondLineOfAddress] = s }
           updates.town?.let { t -> it[town] = t }
           updates.postCode?.let { p -> it[postCode] = p }
       }
       getById(id)
   }

   private fun toUser(row: ResultRow) = User(
       id = row[Users.id].value,
       firstName = row[Users.firstName],
       lastName = row[Users.lastName],
       email = row[Users.email],
       firstLineOfAddress = row[Users.firstLineOfAddress],
       secondLineOfAddress = row[Users.secondLineOfAddress],
       town = row[Users.town],
       postCode = row[Users.postCode]
   )
}

Dependency Injection

Ktor’s way of defining singletons are

val appModule = module {
   single { UserDAO() }
   single { OrderDAO() }
   single<UserService> { UserServiceImpl(get()) }
   single<OrderService> { OrderServiceImpl(get()) }
}

Ktor vs Spring Boot Comparison Summary

Feature Spring Boot Ktor 
Use Case Enterprise app, APIs, monoliths, microservices Lightweight APIs, microservices, Kotlin-first apps
EcoSystem Mature, large ecosystem Smaller but growing
Server startup SpringApplication.run(…) embeddedServer(Netty, …)
Dependency Injection Annotations (@Service, @Autowired,@Bean) install(Koin) { modules(…) }
Serialization/JSON setup Auto-configured with Jackson install(ContentNegotiation) { json(…) }
Database and ORM Auto via Spring Data JPA/Hibernate  @Bean configs.

CrudRepositorry, JpaRepository, Query

Exposed, Manual via Database.connect(…)

Exposed DAO pattern

Routing Controllers using @RequestController, @RequestMapping routing {  get(…) } DSL
Security Spring Security Ktor Auth plugin(basic, JWT, OAuth2 etc)
Testing @SpringBootTest, @MockMvc,@WebTestClientJUnit, Mockito, AssertJ testApplication { … }

JUnit, Kotest, MockK

Code location

https://github.com/ranjesh1/ktor-rest-order/


Spread the love

10 Replies to “Ktor for Spring Boot Developers”

Leave a Reply to Jamie Burman Cancel reply

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