Перейти к основному содержимому

Ktor — первая программа

Разработчику Архитектору

Ktor — первая программа

Эта статья — пошаговый вход в backend на Kotlin — вы поднимете маленький HTTP-сервер, который отвечает на запросы из браузера, curl или мобильного приложения. Код можно повторить без опыта в сетевых протоколах: ниже есть словарь терминов и разбор каждой важной строки.

Ktor — асинхронный фреймворк от JetBrains для HTTP-серверов и HTTP-клиентов. Внутри он опирается на корутины: пока один запрос ждёт сеть или диск, поток JVM может обслуживать другие запросы.

База языка: Первая программа на Kotlin. Экосистема: Экосистема Kotlin. Android и сеть с устройства: мобильный раздел.


Что вы строите

Небольшой REST API для заметок:

Метод HTTPПутьСмысл
GET/health"Сервер жив?" — для мониторинга
GET/notesСписок всех заметок
POST/notesСоздать заметку (тело — JSON)
DELETE/notes/{id}Удалить заметку по номеру

Данные пока хранятся в памяти (mutableListOf) — после перезапуска сервера список обнулится. Для постоянного хранения позже подключают базу данных; логика маршрутов останется похожей.

Почему пример сделан in-memory

Такой формат позволяет сначала понять HTTP-слой и маршрутизацию. После закрепления добавьте репозиторий и БД по статье Работа с базами данных, сохранив те же endpoint'ы.


Словарь терминов

ТерминПростыми словами
HTTPПротокол обмена: клиент (браузер, приложение) шлёт запрос, сервер возвращает ответ с кодом статуса и телом.
REST APIНабор URL ("ресурсов"), к которым обращаются методами GET, POST, DELETE и т.д. Вместо "страниц HTML" часто отдают JSON.
Маршрут (route)Связка "путь + метод" → код обработчика. В Ktor блок routing { get("/health") { ... } }.
JSONТекстовый формат данных: {"id":1,"text":"Привет"}. Удобен для приложений и веба.
СериализацияПревращение объекта Kotlin (Note) в JSON и обратно. Здесь — библиотека kotlinx.serialization.
Engine (движок)Низкоуровневый слой, который слушает порт и принимает TCP-соединения. Netty — популярный выбор на JVM.
Плагин (feature)Подключаемая возможность: разбор JSON, логи, CORS, авторизация. В коде — install(ContentNegotiation).
callКонтекст одного HTTP-запроса: параметры пути, тело, ответ (call.respond).
embeddedServerЗапуск встроенного сервера в том же процессе, что и main — без отдельного Tomcat.

Создание проекта

Вариант 1 — генератор

На start.ktor.io выберите:

  • Engine: Netty
  • Dependencies: Routing, Content Negotiation, kotlinx-serialization

Скачайте архив, откройте в IntelliJ IDEA, выполните ./gradlew run.

./gradlew run

Разбор:

  • ./gradlew запускает Gradle Wrapper из проекта и гарантирует использование согласованной версии Gradle.
  • Команда run собирает проект и стартует приложение через application-плагин.
  • Такой запуск удобен для локальной разработки: изменения кода быстро проверяются без ручной сборки JAR.

Вариант 2 — Gradle вручную

Файл build.gradle.kts в корне проекта:

Код ITЗагрузка примера кода…

Разбор:

  • Блок plugins подключает компилятор Kotlin, сериализацию и поддержку запуска приложения.
  • repositories { mavenCentral() } указывает основной источник зависимостей JVM-экосистемы.
  • В dependencies перечислены серверный core Ktor, движок Netty, JSON-плагин и сериализатор.
  • logback-classic добавляет конфигурируемое логирование запросов и внутренних событий приложения.
  • application { mainClass.set(...) } задаёт точку входа, которую использует gradlew run.

Разбор фрагмента:

  • kotlin("plugin.serialization") — компилятор генерирует код для @Serializable.
  • ktor-server-netty — движок Netty.
  • content-negotiation + serialization-kotlinx-json — автоматический JSON в запросах и ответах.
  • logback — логи в консоль (кто к какому URL обратился).
  • mainClass — класс с функцией main (для файла Application.kt имя станет ApplicationKt).

Модели данных

@Serializable
data class Note(val id: Int, val text: String)

@Serializable
data class NoteCreate(val text: String)

Разбор:

  • @Serializable включает генерацию кода сериализации для преобразования моделей в JSON и обратно.
  • data class Note описывает сущность ответа API с обязательными id и text.
  • data class NoteCreate моделирует входное тело запроса при создании заметки.
  • Разделение входной и выходной моделей упрощает валидацию и развитие контракта API. См. Проверка и валидация.
ЭлементЗачем
@SerializableПомечает класс для kotlinx.serialization — из него можно сделать JSON.
data classАвтоматические equals, copy, удобная печать.
NoteПолная заметка с id — то, что сервер отдаёт клиенту.
NoteCreateТолько текст при создании — клиент ещё не знает id, его выдаёт сервер.

Полный код Application.kt

Код ITЗагрузка примера кода…

Разбор:

  • Импорты io.ktor.server.* подключают API запуска сервера, маршрутов, чтения запросов и отправки ответов.
  • main() поднимает сервер на порту 8080 и регистрирует функцию Application::module как конфигурацию.
  • install(ContentNegotiation) { json() } включает автоматическую JSON-сериализацию для тел запросов и ответов.
  • notes и nextId хранят состояние in-memory и выдают идентификаторы новым заметкам.
  • В routing { ... } объявлены endpoint'ы GET, POST, DELETE и их логика обработки HTTP-вызовов.

Построчный разбор main и модуля

fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}

Разбор:

  • embeddedServer(...) создаёт HTTP-сервер внутри текущего процесса JVM.

  • Netty выбирается как сетевой engine для приёма соединений и обработки запросов.

  • module = Application::module передаёт ссылку на функцию конфигурации приложения.

  • start(wait = true) удерживает главный поток активным, пока сервер не остановлен.

  • embeddedServer — создаёт сервер в текущем JVM-процессе.

  • Netty — конкретный движок.

  • port = 8080 — слушать http://127.0.0.1:8080.

  • module = Application::module — при старте вызовется ваша функция Application.module().

  • main не завершится, пока сервер работает (удобно для ./gradlew run).

start(wait = true)

Разбор:

  • Параметр wait = true переводит запуск в блокирующий режим: приложение остаётся в рабочем состоянии.
  • Для CLI-сценариев это стандартный режим, чтобы процесс не завершился сразу после инициализации.
  • При wait = false сервер стартует асинхронно и управление возвращается вызывающему коду.
fun Application.module() {
install(ContentNegotiation) { json() }

Разбор:

  • Расширение Application.module() объединяет конфигурацию плагинов и маршрутов в одной точке.
  • install(ContentNegotiation) подключает механизм выбора формата данных по HTTP-заголовкам.
  • json() регистрирует JSON-конвертер, чтобы call.receive<T>() и call.respond(...) работали с объектами Kotlin.

Расширение Application.module — идиоматичный стиль Ktor: настройка привязана к жизненному циклу приложения. Плагин Content Negotiation смотрит заголовок Content-Type и превращает тело запроса/ответа в JSON и обратно.

val notes = mutableListOf<Note>()
var nextId = 1

Разбор:

  • mutableListOf<Note>() создаёт изменяемую коллекцию для хранения заметок в оперативной памяти.
  • var nextId = 1 хранит счётчик идентификаторов, который увеличивается при каждом POST.
  • Такой подход удобен для учебного API без подключения БД, миграций и репозиториев.

"База данных" в памяти. nextId++ выдаёт уникальный номер каждой новой заметке (сначала 1, потом 2, …).


Маршруты

GET /health

get("/health") {
call.respond(mapOf("status" to "ok"))
}

Разбор:

  • get("/health") регистрирует маршрут проверки доступности сервиса.
  • call.respond(...) отправляет HTTP-ответ клиенту с телом JSON-объекта.
  • mapOf("status" to "ok") формирует компактный payload, подходящий для мониторинга и probe-проверок.

Клиент получит JSON {"status":"ok"} и код 200 OK. Так проверяют, что процесс запущен (оркестраторы, балансировщики).

GET /notes

call.respond(notes)

Разбор:

  • notes содержит текущее состояние коллекции заметок в памяти.
  • call.respond(notes) сериализует список List<Note> в JSON-массив автоматически через ContentNegotiation.
  • Endpoint предоставляет read-модель API, которую клиент может отобразить или кэшировать.

Список сериализуется в JSON-массив: [{"id":1,"text":"..."}, ...].

POST /notes

val body = call.receive<NoteCreate>()
val note = Note(nextId++, body.text)
notes += note
call.respond(note)

Разбор:

  • call.receive<NoteCreate>() десериализует JSON-тело входящего запроса в типобезопасную модель.
  • Note(nextId++, body.text) создаёт новую сущность и одновременно увеличивает счётчик ID.
  • notes += note добавляет запись в изменяемую коллекцию приложения.
  • call.respond(note) возвращает созданный объект клиенту как подтверждение результата операции.
  1. receive<NoteCreate>() — прочитать тело запроса как JSON в объект Kotlin.
  2. Собрать Note с новым id.
  3. Добавить в список и вернуть созданную заметку клиенту (обычно код 200).

DELETE /notes/{id}

  • {id} в пути — параметр маршрута. Для /notes/5 значение "5" лежит в call.parameters["id"].
  • toIntOrNull() — безопасное преобразование; при мусоре в URL вернём 400 Bad Request.
  • removeIf { it.id == id } — удалить элемент из списка.
  • 204 No Content — успех без тела; 404 — заметки с таким id не было.

Запуск

./gradlew run

Разбор:

  • Команда используется для быстрого локального старта сервера из корня проекта.
  • При запуске Gradle сначала компилирует Kotlin-код и разрешает зависимости.
  • После успешной сборки запускается JVM-процесс с Ktor-приложением и логами в консоль.

В логах появится строка вроде Responding at http://0.0.0.0:8080. Остановка — Ctrl+C в терминале.


Проверка вручную

curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/notes
curl -X POST http://127.0.0.1:8080/notes \
-H "Content-Type: application/json" \
-d '{"text":"Изучить Ktor"}'
curl -X DELETE http://127.0.0.1:8080/notes/1

Разбор:

  • Первые два вызова curl проверяют здоровье сервиса и чтение списка заметок (GET-маршруты).
  • -X POST вместе с -H "Content-Type: application/json" отправляет JSON в endpoint создания заметки.
  • Параметр -d '{...}' передаёт тело запроса, которое сервер читает через receive<NoteCreate>().
  • -X DELETE удаляет конкретный ресурс по ID и помогает проверить корректность статусов 204/404.
КомандаЧто проверяет
ПерваяЖив ли сервер
ВтораяПустой или полный список
ТретьяСоздание с JSON-телом
ЧетвёртаяУдаление по id

Заголовок Content-Type: application/json говорит серверу: тело запроса — JSON. Без него receive<NoteCreate>() может не сработать.


Как запрос проходит через Ktor

Разбор:

  • Диаграмма показывает полный путь запроса: от клиента через Netty и роутер до конкретного обработчика.
  • Узел Routing выбирает нужный endpoint на основе HTTP-метода и URL.
  • Внутри Handler выполняется бизнес-логика — чтение тела, создание заметки, изменение состояния.
  • Завершающий шаг формирует HTTP-ответ с кодом и JSON, который возвращается клиенту.

Плагины (следующий шаг)

ПлагинНазначение
AuthenticationJWT, Basic, OAuth
CORSЗапросы из браузера с другого домена
StatusPagesЕдиный формат ошибок вместо разрозненных respondText
CallLoggingЛог каждого запроса (метод, путь, время)

Подключение похоже на install(ContentNegotiation): объявляете в Application.module() до или после routing.


Ktor и Spring

СтекКогда уместен
KtorKotlin-first, явные маршруты, лёгкий старт, корутины в обработчиках
Spring BootБольшая экосистема (Security, Data, Cloud), команда уже на Spring

Spring на Kotlin: Spring Boot на Kotlin — первая программа. Java-обзор: Spring для Kotlin/Java.


Частые ошибки

СимптомПричина
Connection refusedСервер не запущен или другой порт
415 / пустое тело при POSTНет заголовка Content-Type: application/json
@Serializable не найденНе подключён плагин kotlin("plugin.serialization")
Порт занятУже работает другой процесс на 8080

Что попробовать

  1. Добавить PUT /notes/{id} для редактирования текста.
  2. Подключить CallLogging и посмотреть логи при curl.
  3. Написать клиент на Ktor Client к этому же серверу.

Мини-чеклист качества API

Перед тем как считать первый сервер "готовым", проверьте базовые инженерные пункты:

  • коды ответов согласованы (200/201/204/400/404);
  • ошибки возвращаются в одном формате JSON;
  • есть GET /health и минимум один тест в testApplication;
  • входные данные валидируются до бизнес-логики;
  • логируются метод, путь, статус и время запроса.

Этот чеклист пригодится и для следующей статьи по тестированию — Тестирование на Kotlin.


Связанные материалы


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Первые шаги (маршрут подборки) — Первая программа на Symfony, Compose Multiplatform — первая программа, Первая программа на Laravel, Spring Boot на Kotlin — первая программа, Qt Quick — первая программа на QML, Первая программа на Gin.