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/
Good one
Good guide on Ktor implementaion
Great article, very easy to understand
I recommend this guide for any one who wants to build rest applictaion with Ktor
This is well written article for Ktor
This was exactly I was looking for sample Ktor
Got better understanding of Ktor now
Use of Ktor nicely explained
Nicely explained implementation of Ktor
Excellent article on Ktor