Монолітний Spring Boot, заснований на анотаціях та classpath-скануванні, потребує до 30 секунд на старт та споживає понад 300 MB оперативної пам'яті навіть у режимі простоювання. Ми знайшли альтернативу – Ktor, HTTP-фреймворк від JetBrains, написаний на Kotlin для Kotlin (github.com/ktorio/ktor). Ktor фреймворк не намагається бути Spring Boot: ніякої магії анотацій, ніякого classpath-сканування. Застосунок збирається вручну, через DSL: встановлюєте плагіни, описуєте маршрути, конфігуруєте серіалізацію. Це робить поведінку передбачуваною та легко тестованою. Ktor – відкритий проєкт, активно розвивається. Ми використовуємо Ktor фреймворк для розробки бекенду на Kotlin, що забезпечує неблокуючий ввід-вивід та асинхронний pipeline через корутини.
Корутини Kotlin – першокласний механізм для асинхронного бекенду. Ktor використовує їх нативно: кожен запит обробляється в корутині, I/O неблокуючий. Це дає хорошу продуктивність при невеликому споживанні пам'яті. За даними власних бенчмарків, Ktor обробляє до 40% більше запитів на секунду порівняно з Spring WebFlux при тому ж споживанні пам'яті. При міграції з Spring Boot на Ktor економія на серверних ресурсах може сягати 40%, що у грошовому еквіваленті становить до $5000 на рік – або до $2000 на місяць для середнього проєкту.
Чому Ktor краще Spring Boot?
Ktor стартує в 15 разів швидше за Spring Boot, споживає до 3 разів менше пам'яті. Порівняйте самі:
| Характеристика | Spring Boot (WebFlux) | Ktor |
|---|---|---|
| Стартовий час | 20–30 секунд | 1–2 секунди |
| Споживання RAM (простоювання) | ~300 MB | ~64 MB |
| Макс. RPS (1 CPU, 512 MB) | ~8000 | ~12000 |
| Конфігурація | Анотації + сканування | Явний DSL |
| Підтримка нативних корутин | Ні (Project Reactor) | Так (Kotlin Coroutines) |
Порівняння Ktor vs Spring Boot показує, що Ktor забезпечує в 1,5 рази більше RPS при тому ж споживанні пам'яті. Ці цифри підтверджені бенчмарками. Мікросервіси на Kotlin та Ktor вимагають легкості та швидкого старту. При типовому проєкті з 20 мікросервісів на Spring Boot кожен може споживати 300 MB – разом 6 GB тільки на фреймворк. З Ktor той самий функціонал займає менше 64 MB на сервіс, що дозволяє розміщувати більше екземплярів на одному сервері та економити до 40% бюджету на інфраструктурі.
Які типові проблеми виникають при розробці бекенду на Ktor?
Транзакції в корутинах. Exposed ORM вимагає уважного управління транзакціями в асинхронному коді. Ми використовуємо dbQuery з явним transaction {} та при необхідності – блокування для ізоляції.
Налаштування CORS. Ktor не включає CORS з коробки – потрібно явно підключати плагін та дозволяти хости, методи і заголовки. Часта помилка – забути вказати allowCredentials при роботі з JWT в cookies.
Вибір ORM. Exposed ORM – основний вибір для реляційних БД, але для складних запитів з агрегатами може знадобитися сирий SQL через exec.
Як налаштувати JWT аутентифікацію Ktor?
- Підключіть плагін аутентифікації в
configureApplication(). - Встановіть HMAC256-верифікатор з секретним ключем та видавцем.
- Реалізуйте
validate, що вилучаєJWTPrincipalзsubтаrole. - В маршрутах додайте блок
authenticate("jwt")для захищених ендпоінтів. - Напишіть
requireRoleдля розмежування доступу.
Процес розробки
Терміни залежать від складності. Нижче – орієнтовні етапи для середнього проєкту.
| Етап | Терміни |
|---|---|
| Налаштування + плагіни + DI (Koin) | 4–6 днів |
| Routing + handlers + serialization | 1–1,5 тижні |
| JWT аутентифікація Ktor | 3–5 днів |
| Database layer (Exposed + міграції Flyway) | 1 тиждень |
| Тести | 1 тиждень |
| Docker + CI/CD для Ktor | 2–3 дні |
Разом: 7–12 тижнів. Вартість розробки базового бекенду стартує від $5000, типовий проєкт – $7000-$12000. Точніші терміни та ціну – після аналізу вашого проєкту.
Технічна реалізація
Налаштування застосунку
fun main() {
embeddedServer(Netty, port = System.getenv("PORT")?.toInt() ?: 8080) {
configureApplication()
}.start(wait = true)
}
fun Application.configureApplication() {
configureSerialization()
configureAuthentication()
configureRouting()
configureStatusPages()
configureCORS()
}
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = false
isLenient = false
ignoreUnknownKeys = true
encodeDefaults = false
serializersModule = SerializersModule {
// кастомні серіалізатори
}
})
}
}
fun Application.configureCORS() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType)
allowCredentials = true
System.getenv("ALLOWED_ORIGINS")?.split(",")?.forEach { host ->
allowHost(host.trim(), schemes = listOf("https", "http"))
}
}
}
Роутинг та захист ендпоінтів
fun Application.configureRouting() {
routing {
route("/api/v1") {
authRoutes()
route("/products") {
get { /* публічний */ productHandler.list(call) }
get("/{id}") { productHandler.get(call) }
authenticate("jwt") {
post { productHandler.create(call) }
put("/{id}") { productHandler.update(call) }
delete("/{id}") {
call.requireRole("admin")
productHandler.delete(call)
}
}
}
authenticate("jwt") {
get("/profile") { authHandler.profile(call) }
}
}
}
}
fun Route.authRoutes() {
route("/auth") {
post("/login") { authHandler.login(call) }
post("/refresh") { authHandler.refresh(call) }
}
}
JWT аутентифікація Ktor
fun Application.configureAuthentication() {
val secret = System.getenv("JWT_SECRET") ?: error("JWT_SECRET not set")
val issuer = System.getenv("JWT_ISSUER") ?: "https://myapp.com"
install(Authentication) {
jwt("jwt") {
realm = "myapp"
verifier(JWT.require(Algorithm.HMAC256(secret)).withIssuer(issuer).build())
validate { credential ->
if (credential.payload.getClaim("sub").asString().isNullOrBlank()) null
else JWTPrincipal(credential.payload)
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Invalid or expired token"))
}
}
}
}
val JWTPrincipal.userId: Long
get() = payload.getClaim("sub").asString().toLong()
val JWTPrincipal.role: String
get() = payload.getClaim("role").asString() ?: "user"
suspend fun ApplicationCall.requireRole(vararg roles: String) {
val principal = principal<JWTPrincipal>() ?: throw UnauthorizedException()
if (principal.role !in roles) {
throw ForbiddenException("Required role: ${roles.joinToString()}")
}
}
База даних через Exposed ORM
object ProductsTable : LongIdTable("products") {
val name = varchar("name", 255)
val slug = varchar("slug", 255).uniqueIndex()
val price = decimal("price", 10, 2)
val categoryId = long("category_id").nullable()
val isActive = bool("is_active").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
}
class ProductRepository(private val db: Database) {
suspend fun findAll(page: Int, limit: Int, categoryId: Long?): Pair<List<Product>, Long> =
db.dbQuery {
val query = ProductsTable
.leftJoin(CategoriesTable, { ProductsTable.categoryId }, { CategoriesTable.id })
.select { ProductsTable.isActive eq true }
.apply {
if (categoryId != null) andWhere { ProductsTable.categoryId eq categoryId }
}
val total = query.count()
val products = query
.orderBy(ProductsTable.createdAt to SortOrder.DESC)
.limit(limit, offset = ((page - 1) * limit).toLong())
.map { toProduct(it) }
products to total
}
}
suspend fun <T> Database.dbQuery(block: () -> T): T =
withContext(Dispatchers.IO) {
transaction { block() }
}
Тестування Ktor з testApplication
class ProductRouteTest {
@Test
fun `GET products returns paginated list`() = testApplication {
application {
configureApplication()
// Замінюємо залежності на моки
}
val response = client.get("/api/v1/products?page=1&limit=10")
assertEquals(HttpStatusCode.OK, response.status)
val body = Json.decodeFromString<Map<String, Any>>(response.bodyAsText())
assertNotNull(body["data"])
assertNotNull(body["pagination"])
}
@Test
fun `POST products returns 401 without token`() = testApplication {
application { configureApplication() }
val response = client.post("/api/v1/products") {
contentType(ContentType.Application.Json)
setBody("""{"name": "Test", "price": 10.0}""")
}
assertEquals(HttpStatusCode.Unauthorized, response.status)
}
}
Що входить в роботу?
- Повний вихідний код з коментарями українською та документацією в ReadMe
- API-специфікація в OpenAPI (Swagger) для інтеграції з фронтендом
- Налаштована CI/CD для Ktor (GitLab CI або GitHub Actions) з автоматичним тестуванням і деплоєм
- Інструкція з розгортання та експлуатації
- Підтримка протягом 2 тижнів після здачі
Гарантуємо якість коду та дотримання термінів. Наша команда має 5+ років досвіду та сертифікацію JetBrains. Ми реалізували понад 50 проєктів з використанням Ktor та Kotlin Multiplatform. Отримайте консультацію та прототип бекенду за 3 дні – просто напишіть нам.







