Введение
Kotlin — это современный статически типизированный язык программирования, который работает на виртуальной машине Java (JVM) и полностью совместим с Java-экосистемой. Благодаря своей лаконичности, безопасности и мощным функциональным возможностям, Kotlin стал одним из самых популярных языков для разработки серверных приложений, особенно после того, как Google выбрал его в качестве основного языка для Android.
В этой статье мы создадим полноценное REST API на Kotlin, используя легковесный фреймворк Ktor и библиотеку для работы с базами данных Exposed. Мы реализуем простой сервис для управления задачами (To-Do List) с поддержкой всех CRUD-операций (создание, чтение, обновление, удаление). Весь процесс займет у вас не более 30 минут, если вы уже установили IntelliJ IDEA и JDK.
1. Настройка проекта
Для начала создадим новый проект Kotlin с поддержкой Gradle. Откройте IntelliJ IDEA и выберите File → New → Project. В мастере создания проекта выберите Kotlin → JVM.
Шаг 1.1: Структура build.gradle.kts
Добавьте следующие зависимости в файл build.gradle.kts:
plugins {
kotlin("jvm") version "1.9.0"
id("io.ktor.plugin") version "2.3.0"
kotlin("plugin.serialization") version "1.9.0"
}
dependencies {
implementation("io.ktor:ktor-server-core:2.3.0")
implementation("io.ktor:ktor-server-netty:2.3.0")
implementation("io.ktor:ktor-server-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
implementation("org.jetbrains.exposed:exposed-core:0.41.1")
implementation("org.jetbrains.exposed:exposed-dao:0.41.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")
implementation("com.h2database:h2:2.1.214")
implementation("ch.qos.logback:logback-classic:1.4.7")
}
Эти зависимости включают: сам фреймворк Ktor, поддержку JSON-сериализации, библиотеку Exposed для работы с БД и встроенную базу данных H2 для тестирования.
Шаг 1.2: Точка входа
Создайте файл Application.kt в пакете com.example:
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import com.example.plugins.*
fun main() {
embeddedServer(Netty, port = 8080) {
configureSerialization()
configureRouting()
}.start(wait = true)
}
2. Настройка сериализации и маршрутизации
Шаг 2.1: Плагины приложения
Создайте директорию plugins и файл Serialization.kt:
package com.example.plugins
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
Шаг 2.2: Определение модели данных
Создайте файл Models.kt:
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class Task(
val id: Int = 0,
val title: String,
val description: String = "",
val completed: Boolean = false
)
3. Работа с базой данных
Шаг 3.1: Настройка подключения
Создайте файл DatabaseFactory.kt:
package com.example.database
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DatabaseFactory {
fun init() {
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(TasksTable)
}
}
}
Шаг 3.2: Определение таблицы
В том же пакете создайте TasksTable.kt:
package com.example.database
import org.jetbrains.exposed.sql.Table
object TasksTable : Table("tasks") {
val id = integer("id").autoIncrement()
val title = varchar("title", 255)
val description = varchar("description", 1024).default("")
val completed = bool("completed").default(false)
override val primaryKey = PrimaryKey(id)
}
4. Реализация CRUD-маршрутов
Теперь самое интересное — создадим обработчики запросов. Отредактируйте файл Routing.kt в пакете plugins:
package com.example.plugins
import com.example.database.DatabaseFactory
import com.example.database.TasksTable
import com.example.models.Task
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
fun Application.configureRouting() {
DatabaseFactory.init()
routing {
route("/api/tasks") {
// GET: Получить все задачи
get {
val tasks = transaction {
TasksTable.selectAll().map { row ->
Task(
id = row[TasksTable.id],
title = row[TasksTable.title],
description = row[TasksTable.description],
completed = row[TasksTable.completed]
)
}
}
call.respond(tasks)
}
// GET: Получить задачу по ID
get("{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@get
}
val task = transaction {
TasksTable.selectAll().where { TasksTable.id eq id }
.singleOrNull()?.let {
Task(it[TasksTable.id], it[TasksTable.title], it[TasksTable.description], it[TasksTable.completed])
}
}
if (task != null) call.respond(task)
else call.respond(HttpStatusCode.NotFound, "Task not found")
}
// POST: Создать новую задачу
post {
val task = call.receive<Task>()
val id = transaction {
TasksTable.insert {
it[title] = task.title
it[description] = task.description
it[completed] =