В этой статье мы разберём, как написать собственный сервер на Kotlin с его сахарным, удобным синтаксисом, подключить базу данных, создать несколько эндпоинтов, а затем всего за пару минут захостить и сервер, и базу. В итоге у нас получится полноценная связка «сервер + БД», готовая к использованию в реальном проекте.
Для начала нам понадобится среда разработки. Я буду работать в IntelliJ IDEA Ultimate, но если у вас Community-версия — не проблема. В этом случае вы можете сгенерировать Ktor-проект прямо на сайте Ktor:
https://ktor.io/
Если же вы используете Ultimate-версию — можете создать сервер прямо из IDE. В любом случае, повторяйте за мной и установите необходимые библиотеки, которые мы будем использовать далее.

com.ваш_выбор или com.вашеимя.
GET /, GET /hello/{name} и POST /echo.

Мы будем работать с базой данных PostgreSQL, а для выполнения запросов и взаимодействия с БД воспользуемся фреймворком Exposed — удобным DSL от JetBrains.
Когда всё настроено, можно начинать. Нажимаем Create, ждём генерацию проекта, затем синхронизируем наш Gradle и запускаем файл Application.kt.

В дальнейшем наш сервер будет запускать все наши маршруты (или endpoints) внутри функции Application.module() { ... }.
А теперь переходим к самому интересному — работе с базой данных.
Наша БД будет уже задеплоена на сервере. Я использую сервис Railway, поэтому покажу процесс на его примере: https://railway.com/
Если у вас есть свой хостинг или провайдер — используйте его. Я работаю с Railway уже почти 3 года, написал на нём кучу бизнес-решений, и он ни разу меня не подвёл.
Переходим к созданию базы:
New → Database → PostgreSQL

Работу с Redis и JWT я разберу в отдельной статье — там мы поговорим о кешировании, уменьшении нагрузки на базу данных и правильной работе с токенами. В этой же статье сосредоточимся только на сервере и PostgreSQL, чтобы не перегружать материал.

Дальше идём в Variables и спускаемся вниз. Здесь мы будем хранить данные, необходимые для подключения нашего сервера к базе данных, чтобы о�� мог работать с ней и выполнять запросы.

Теперь переходим на сервер и открываем файл Databases.kt.
В нём вы найдёте мой код с комментариями, которые помогут понять, что и как нужно делать.
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object DatabaseConfig {
fun init(): Boolean {
return try {
Database.connect(
url = "jdbc:postgresql://caboose.proxy.rlwy.net:19083/railway",
// JDBC-ссылка к БД Railway (домен caboose.proxy.rlwy.net, порт 19083)
// Данные берутся из переменных окружения RAILWAY_TCP_PROXY_DOMAIN и RAILWAY_TCP_PROXY_PORT
driver = "org.postgresql.Driver", // драйвер PostgreSQL
user = "postgres", // PGUSER -> postgres
password = "свой" // пароль из PGPASSWORD
)
transaction {
// Здесь можно создавать или инициализировать таблицы
}
println("✅ Подключение к БД успешно!")
true
} catch (e: Exception) {
println("❌ Ошибка подключения к БД: ${e.message}")
false
}
}
}
Если всё настроено правильно, перед запуском сервера нужно предварительно инициализировать базу данных. После этого можно запускать сервер.

Дальше будем создавать таблицы и инициализировать их в нашей базе данных.
Мы уже подключили библиотеку Exposed, теперь создадим файл Tables.kt.
В нём будет наш код, как описывать таблицы с помощью Exposed. Пока сосредоточимся на таблице Users, чтобы работать с ней в дальнейшем.
// 1) Автоинкрементный integer primary key
object Users : Table("users") {
val id = integer("id").autoIncrement() // INT AUTO_INCREMENT
val username = varchar("username", 50).uniqueIndex()
val email = varchar("email", 255).nullable()
override val primaryKey = PrimaryKey(id) // явно, но autoIncrement уже даёт PK
}
// 2) UUID primary key (если хочешь распределённые id)
object Sessions : Table("sessions") {
val id = uuid("id").clientDefault { UUID.randomUUID() } // UUID default
val userId = reference("user_id", Users.id, onDelete = ReferenceOption.CASCADE)
val token = varchar("token", 128).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
// 3) Composite primary key (например, связь many-to-many)
object UserRoles : Table("user_roles") {
val userId = reference("user_id", Users.id, onDelete = ReferenceOption.CASCADE)
val role = varchar("role", 50)
override val primaryKey = PrimaryKey(userId, role, name = "PK_UserRole")
}
// 4) Таблица с внешним ключом и разными типами колонок
object Orders : Table("orders") {
val id = long("id").autoIncrement() // long для больших чисел
val userId = reference("user_id", Users.id)
val total = decimal("total", precision = 10, scale = 2).default(0.toBigDecimal())
val status = varchar("status", 20).default("NEW") // можно хранить enum как string
val notes = text("notes").nullable()
override val primaryKey = PrimaryKey(id)
}
// 5) Пример enum-like с проверкой (можно хранить int или string)
enum class CandyType { CHOCOLATE, JELLY, CARAMEL }
object Candies : Table("candies") {
val id = integer("id").autoIncrement()
val name = varchar("name", 100)
val type = varchar("type", 20).default(CandyType.CHOCOLATE.name) // store enum name
override val primaryKey = PrimaryKey(id)
}
Инициализируем её в файле Databases.kt — после сборки и запуска сервера таблица автоматически создастся в базе данных.
transaction {
// Здесь можно создавать или инициализировать таблицы
SchemaUtils.create(Users)
}
Здесь мы создаём модель данных, с которой будем работать. Она нужна, чтобы корректно оформлять тело запроса (body) и обрабатывать данные в нашем сервере.
Модель данных можно разместить в отдельном файле Serialization.kt — так код будет аккуратным и логично структурированным.
@Serializable
data class CreateUserRequest(
val username: String,
val email: String? = null
)
@Serializable
data class UserDTO(
val id: Int,
val username: String,
val email: String? = null
)
В файле мы будем писать Routing. Создадим два маршрута: один для добавления пользователя, второй — для получения всех пользователей.
fun Application.configureRouting() {
routing {
post("/users") {
// Получаем тело запроса и десериализуем его в объект CreateUserRequest
val request = call.receive<CreateUserRequest>()
// Открываем транзакцию Exposed для работы с базой данных
val userId = transaction {
// Вставляем нового пользователя в таблицу Users
Users.insert {
it[username] = request.username // записываем имя пользователя
it[email] = request.email // записываем email (может быть null)
} get Users.id // получаем id только что вставленной записи
}
// Отправляем клиенту ответ 201 Created с JSON, содержащим id нового пользователя
call.respond(HttpStatusCode.Created, mapOf("id" to userId))
}
// Обрабатываем GET-запрос по пути "/users"
get("/users") {
// Открываем транзакцию Exposed для работы с базой данных
val all = transaction {
// Получаем все записи из таблицы Users
Users.selectAll().map { row ->
// Преобразуем каждую запись в объект UserDTO
UserDTO(
id = row[Users.id], // id пользователя (Int)
username = row[Users.username],// имя пользователя (String)
email = row[Users.email] // email пользователя (String?), может быть
)
}
}
// Отправляем клиенту результат в виде JSON
// Здесь используется kotlinx.serialization для сериализации списка UserDTO
call.respond(all)
}
}
}
Не забудем добавить это в наш Application.kt, чтобы маршруты заработали.
fun Application.module() {
DatabaseConfig.init() // БД
configureRouting() // POST и GET
}
После того как маршруты настроены, мы можем протестировать запросы в Postman.

Погнали деплоить!
Мы будем использовать Railway (или любой другой сервис на ваш выбор), но я покажу пример именно на Railway: https://railway.com/.
Я работаю с этим сервисом уже 3 года, пишу бэкенд и Android-приложения, и мне всегда было просто быстро развернуть проект: написать код, обновить и сразу увидеть его работающим в интернете — без сложных настроек, «из коробки».
Railway — это суперудобно. В дальнейшем мы сможем подключать к нему другие сервисы: Redis, Kafka и многое другое. Всё делается буквально в два клика. Сообщество отличное, есть поддержка, и особенно мне нравится встроенная метрика: можно смотреть логи и анализировать работу сервиса.
Для старта дают 5$, что достаточно, чтобы потыкать и протестировать проект. В дальнейшем можно перейти на план Hobby.
Кстати, вы можете использовать мою реферальную ссылку и получить 20$ при оплате: это около 4 месяцев бесплатного использования. Я тоже получу бонус от Railway, так что всем выгодно ?
Ссылка для регистрации с бонусом: https://railway.com?referralCode=2026
Код: 2026
Погнали внутрь проекта. В папке Gradle обратите внимание на поле group. У меня оно выглядит так:
group = "com.ilya"
У вас может быть другое, например:
group = "com.ivan"
Делайте так, как в моём примере Gradle:
val exposed_version: String by project
val h2_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val postgres_version: String by project
plugins {
kotlin("jvm") version "2.2.20"
id("io.ktor.plugin") version "3.3.2"
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
}
group = "com.ilya"
version = "0.0.1"
application {
mainClass = "io.ktor.server.netty.EngineMain"
}
application {
mainClass.set("io.ktor.server.netty.EngineMain")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}
tasks {
named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveFileName.set("app.jar")
mergeServiceFiles()
}
}
tasks {
shadowJar {
archiveFileName.set("app.jar")
mergeServiceFiles()
}
}
application {
mainClass.set("com.ilya.ApplicationKt") // Убедитесь, что это правильная точка входа
}
tasks {
create("stage").dependsOn("installDist")
}
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
manifest {
attributes["Main-Class"] = "com.ilya.ApplicationKt" // Точка входа
}
mergeServiceFiles()
archiveClassifier.set("")
}
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
archiveFileName.set("server.jar") // Установите желаемое имя файла
}
repositories {
mavenCentral()
}
dependencies {
implementation("io.ktor:ktor-server-core")
implementation("io.ktor:ktor-server-content-negotiation")
implementation("io.ktor:ktor-serialization-kotlinx-json")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:$h2_version")
implementation("org.postgresql:postgresql:$postgres_version")
implementation("io.ktor:ktor-server-netty")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-config-yaml")
testImplementation("io.ktor:ktor-server-test-host")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
Если в Gradle вы заменили все вхождения com.ilya на свой group, делаем дальше:
Собираем Gradle — проверяем, что проект компилируется без ошибок.
Коммитим все изменения в наш репозиторий на GitHub.
-
Переходим на Railway.
В том же разделе, где мы создавали базу данных, или в новом (если база в другом месте), подключаем репозиторий с нашим сервером.
Выбираем репозиторий на GitHub и запускаем деплой.
После этого Railway автоматически соберёт и запустит ваш сервер.



В поле Start Command нужно указать всего одну команду для сборки и запуска сервера. Вот пример для Railway:

-Xmx — это параметр, который задаёт максимальный размер кучи JVM. Пока будем использовать 512m, но при необходимости его можно увеличить до 1024m.
Всё зависит от того, что у вас указано в Gradle — именно от команд сборки и запуска, прописанных там, будет выполняться сервер на Railway.
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
archiveFileName.set("server.jar") // Установите желаемое имя файла
}



Теперь можно протестировать наш сервер в Postman. Проверим, что все маршруты работают корректно, и убедимся, что данные добавляются и читаются из базы.

В этой статье мы научились:
писать сервер на Kotlin с использованием Ktor,
подключать его к базе данных PostgreSQL для хранения данных,
создавать маршруты (endpoints) и работать с ними,
и всего за 5 минут задеплоить сервер с базой на Railway.
Теперь вы можете спокойно добавлять новые маршруты и обработку данных в локальной среде — всё, что собирается на вашей машине, после коммита и пуша на GitHub, автоматически будет собираться и запускаться на сервере благодаря удобному CI/CD на Railway.
Не забывайте использовать мою реферальную ссылку при первой оплате на Railway: вы получите 20$, а я — 5$.
В следующей статье мы разберём:
проверку пользователей через Firebase по UID,
создание полноценной системы JWT с Redis для кеширования и ускоренного ответа сервера,
а также дополнительные возможности для масштабирования и оптимизации.
Свои идеи и предложения тоже присылайте — буду рад их обсудить.
Надеюсь, статья была полезной, и у вас всё получилось с первого раза, как и у меня! Теперь вы знаете, как быстро создавать, тестировать и деплоить сервер на Kotlin. ?