Розробка бекенду сайту на Kotlin (Ktor)

Тяжкі фреймворки сповільнюють запуск і витрачають ресурси сервера, що критично для сучасних веб-проєктів. Ми розробляємо бекенд на Kotlin з використанням Ktor — легкого асинхронного фреймворку, який стартує за секунди та ефективно використовує пам'ять. Наша команда реалізує проєкт під ключ: від проєктування архітектури до впровадження та подальшої підтримки, забезпечуючи надійне та масштабоване рішення.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка бекенду сайту на Kotlin (Ktor)
Складний
від 1 тижня до 3 місяців
Часті запитання

Монолітний 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?

  1. Підключіть плагін аутентифікації в configureApplication().
  2. Встановіть HMAC256-верифікатор з секретним ключем та видавцем.
  3. Реалізуйте validate, що вилучає JWTPrincipal з sub та role.
  4. В маршрутах додайте блок authenticate("jwt") для захищених ендпоінтів.
  5. Напишіть 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 дні – просто напишіть нам.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060