В этой статье мы разберём, как написать собственный сервер на Kotlin с его сахарным, удобным синтаксисом, подключить базу данных, создать несколько эндпоинтов, а затем всего за пару минут захостить и сервер, и базу. В итоге у нас получится полноценная связка «сервер + БД», готовая к использованию в реальном проекте.

Для начала нам понадобится среда разработки. Я буду работать в IntelliJ IDEA Ultimate, но если у вас Community-версия — не проблема. В этом случае вы можете сгенерировать Ktor-проект прямо на сайте Ktor:
https://ktor.io/

Если же вы используете Ultimate-версию — можете создать сервер прямо из IDE. В любом случае, повторяйте за мной и установите необходимые библиотеки, которые мы будем использовать далее.

Имя проекта можете выбрать любое. Поле Group также указывается произвольно — обычно используют формат com.ваш_выбор или com.вашеимя.
Имя проекта можете выбрать любое. Поле Group также указывается произвольно — обычно используют формат com.ваш_выбор или com.вашеимя.
Добавляем Routing в Ktor Routing — это то, что превращает ваш сервер в API: вы описываете пути (эндпоинты) и то, как на них реагировать. Покажу минимальную рабочую конфигурацию с поддержкой JSON (kotlinx.serialization) и парой э��дпоинтов: GET /, GET /hello/{name} и POST /echo.
Добавляем Routing в Ktor Routing — это то, что превращает ваш сервер в API: вы описываете пути (эндпоинты) и то, как на них реагировать. Покажу минимальную рабочую конфигурацию с поддержкой JSON (kotlinx.serialization) и парой эндпоинтов: 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

Postgres удобная бд и самая популярнаяя
Postgres удобная бд и самая популярнаяя

Работу с 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, делаем дальше:

  1. Собираем Gradle — проверяем, что проект компилируется без ошибок.

  2. Коммитим все изменения в наш репозиторий на GitHub.

  3. Переходим на Railway.

    • В том же разделе, где мы создавали базу данных, или в новом (если база в другом месте), подключаем репозиторий с нашим сервером.

    • Выбираем репозиторий на GitHub и запускаем деплой.

После этого Railway автоматически соберёт и запустит ваш сервер.

Советую выбрать Нидерланды в качестве региона, если вы живёте в Европе — так задержка будет минимальной и сервер будет работать быстрее.
Советую выбрать Нидерланды в качестве региона, если вы живёте в Европе — так задержка будет минимальной и сервер будет работать быстрее.

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

-Xmx — это параметр, который задаёт максимальный размер кучи JVM. Пока будем использовать 512m, но при необходимости его можно увеличить до 1024m.
-Xmx — это параметр, который задаёт максимальный размер кучи JVM. Пока будем использовать 512m, но при необходимости его можно увеличить до 1024m.
Важно не нзабудте сгенерировать наш домен а то как оброщатся к нашему сервру
Важно не нзабудте сгенерировать наш домен а то как оброщатся к нашему сервру

Всё зависит от того, что у вас указано в Gradle — именно от команд сборки и запуска, прописанных там, будет выполняться сервер на Railway.

tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
    archiveFileName.set("server.jar") // Установите желаемое имя файла
}
Ждём пару минут, пока сервер соберётся и развернётся на Railway.
Ждём пару минут, пока сервер соберётся и развернётся на Railway.
Вот и всё
Вот и всё
Тестируем наш срервре в postman

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

В этой статье мы научились:

  • писать сервер на Kotlin с использованием Ktor,

  • подключать его к базе данных PostgreSQL для хранения данных,

  • создавать маршруты (endpoints) и работать с ними,

  • и всего за 5 минут задеплоить сервер с базой на Railway.

Теперь вы можете спокойно добавлять новые маршруты и обработку данных в локальной среде — всё, что собирается на вашей машине, после коммита и пуша на GitHub, автоматически будет собираться и запускаться на сервере благодаря удобному CI/CD на Railway.

Не забывайте использовать мою реферальную ссылку при первой оплате на Railway: вы получите 20$, а я — 5$.

В следующей статье мы разберём:

  • проверку пользователей через Firebase по UID,

  • создание полноценной системы JWT с Redis для кеширования и ускоренного ответа сервера,

  • а также дополнительные возможности для масштабирования и оптимизации.

  • Свои идеи и предложения тоже присылайте — буду рад их обсудить.

Надеюсь, статья была полезной, и у вас всё получилось с первого раза, как и у меня! Теперь вы знаете, как быстро создавать, тестировать и деплоить сервер на Kotlin. ?

Комментарии (0)