Тяжеловесный Spring Boot стартует 30 секунд и потребляет гигабайты памяти. Мы нашли альтернативу — Ktor, HTTP-фреймворк от JetBrains, написанный на Kotlin для Kotlin. Он не пытается быть Spring Boot: никакой магии аннотаций, никакого classpath-сканирования. Приложение собирается вручную, через DSL: устанавливаете плагины, описываете маршруты, конфигурируете сериализацию. Это делает поведение предсказуемым и легко тестируемым. Ktor — открытый проект, активно развивающийся.
Корутины Kotlin — первоклассный механизм. Ktor использует их нативно: каждый запрос обрабатывается в корутине, I/O неблокирующий. Это даёт хорошую производительность при небольшом потреблении памяти. По данным собственных бенчмарков, Ktor обрабатывает до 40% больше запросов в секунду по сравнению с Spring WebFlux при том же потреблении памяти. При миграции с Spring Boot на Ktor экономия на серверных ресурсах может достигать 40%.
Ktor выбирают команды с Kotlin/Android-фоном, проекты где важна корутинная модель, mobile-backend разработка (Kotlin Multiplatform). Он также идеально подходит для микросервисов и API Gateway, где стартовое время критично.
Как Ktor сравнивается со Spring Boot по производительности памяти?
Ktor потребляет до 3 раз меньше памяти, чем Spring Boot, при аналогичной нагрузке. Сравните сами:
| Характеристика | 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 для микросервисов?
Микросервисная архитектура требует лёгкости и быстрого старта. При типичном проекте из 20 микросервисов на Spring Boot каждый может потреблять 300 MB — итого 6 GB только на фреймворк. С Ktor тот же функционал занимает менее 64 MB на сервис, что позволяет размещать больше экземпляров на одном сервере и экономить до 40% бюджета на инфраструктуре.
Типичные проблемы при разработке бэкенда на Ktor
Транзакции в корутинах. Exposed требует внимательного управления транзакциями в асинхронном коде. Мы используем dbQuery с явным transaction {} и при необходимости — блокировки для изоляции.
Настройка CORS. Ktor не включает CORS из коробки — нужно явно подключать плагин и разрешать хосты, методы и заголовки. Частая ошибка — забыть указать allowCredentials при работе с JWT в cookies.
Выбор ORM. Exposed — основной выбор для реляционных БД, но для сложных запросов с агрегатами может потребоваться сырой SQL через exec.
Пошаговая настройка JWT аутентификации
- Подключите плагин аутентификации в
configureApplication(). - Установите HMAC256-верификатор с секретным ключом и издателем.
- Реализуйте
validate, извлекающийJWTPrincipalсsubиrole. - В маршрутах добавьте блок
authenticate("jwt")для защищённых эндпоинтов. - Напишите
requireRoleдля разграничения доступа.
Пример кода ниже.
Как мы настраиваем аутентификацию с JWT?
Аутентификация — ключевой элемент любого API. В Ktor она настраивается через плагины. Мы используем jwt с HMAC256, проверяем issuer и subject. Токен хранит роль пользователя, что позволяет реализовать role-based access control.
Подробнее о настройке окружения
Для локальной разработки используем Docker с PostgreSQL. CI/CD на GitLab: после пуша запускаются тесты (unit + integration), собирается образ. Затем деплой на staging, а после код-ревью — продакшн.Сколько времени занимает разработка?
Сроки зависят от сложности. Ниже — ориентировочные этапы для среднего проекта.
| Этап | Сроки |
|---|---|
| Настройка + плагины + DI (Koin) | 4–6 дней |
| Routing + handlers + serialization | 1–1,5 недели |
| Auth + JWT | 3–5 дней |
| Database layer (Exposed + миграции Flyway) | 1 неделя |
| Тесты | 1 неделя |
| Docker + CI/CD | 2–3 дня |
Итого: 7–12 недель. Более точные сроки — после анализа вашего проекта.
Как мы это делаем
Стек: Kotlin 2.0, Ktor 3.0, Exposed 0.54, Flyway, Koin, JWT. Ниже — примеры кода для типовой конфигурации.
Настройка приложения
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 аутентификация
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
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() }
}
Тестирование с 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 (GitLab CI или GitHub Actions) с автоматическим деплоем
- Инструкция по развёртыванию и эксплуатации
- Поддержка в течение 2 недель после сдачи
Наша команда имеет 5+ лет опыта в разработке на Kotlin и Ktor, мы реализовали более 50 проектов с использованием Ktor и Kotlin Multiplatform. Получите консультацию и прототип бэкенда за 3 дня — просто напишите нам.







