Fullstack-разработка на Kotlin: от настройки до деплоя

Практический курс по созданию веб-сервисов на Kotlin, охватывающий архитектуру базы данных, бизнес-логику, REST API и интеграцию с клиентской частью.

1. Инициализация проекта, конфигурация сборки и основы развертывания приложения

Инициализация проекта, конфигурация сборки и основы развертывания приложения

Добро пожаловать на курс Fullstack-разработка на Kotlin: от настройки до деплоя. Это первая статья нашего цикла, в котором мы пройдем путь от пустой папки до работающего веб-приложения с базой данных, API и клиентской частью.

Сегодня мы заложим фундамент. Многие новички недооценивают этап настройки, стремясь скорее написать первый код. Однако правильно инициализированный проект и настроенная среда сборки сэкономят вам часы отладки в будущем. Мы разберем, как создать проект на Spring Boot с использованием языка Kotlin, настроим систему сборки Gradle и научимся упаковывать наше приложение в Docker-контейнер.

Инструментарий разработчика

Прежде чем мы начнем, убедитесь, что у вас установлены необходимые инструменты. Для разработки на Kotlin нам понадобится надежная база.

Вам потребуются:

  • JDK (Java Development Kit) версии 17 или 21. Это набор инструментов для разработки на языке Java и других языках JVM (включая Kotlin). Мы выбираем LTS (Long Term Support) версии для стабильности.
  • IntelliJ IDEA. Это лучшая среда разработки (IDE) для Kotlin, созданная той же компанией JetBrains, что и сам язык. Версии Community Edition будет достаточно.
  • Docker. Платформа для контейнеризации, которая позволит нам запускать приложение в изолированной среде.
  • Создание проекта через Spring Initializr

    В мире Java и Kotlin стандартом для создания серверных приложений является фреймворк Spring Boot. Он берет на себя огромную часть рутинной конфигурации, позволяя разработчику сосредоточиться на бизнес-логике.

    Для генерации каркаса проекта мы воспользуемся инструментом Spring Initializr. Это веб-сервис (интегрированный также прямо в IntelliJ IDEA), который создает структуру папок и файлы конфигурации.

    Параметры генерации

    При создании нового проекта выберите следующие настройки:

    * Project: Gradle - Kotlin. Мы будем использовать систему сборки Gradle с DSL (предметно-ориентированным языком) на Kotlin. Это современный стандарт, который дает автодополнение и проверку типов прямо в файлах настройки сборки. * Language: Kotlin. * Spring Boot: Выберите последнюю стабильную версию (без пометок SNAPSHOT или M1). * Group: Например, com.example. Это уникальный идентификатор вашей организации или проекта. * Artifact: Например, kotlin-fullstack. Это название самого приложения. * Packaging: Jar. * Java: 17 или 21 (в зависимости от того, что вы установили).

    Подключение зависимостей

    На этом этапе нам нужно добавить библиотеки, которые понадобятся в будущем. В Spring Boot они называются «стартерами» (Starters).

    Добавьте следующие зависимости:

  • Spring Web. Необходима для создания REST API и обработки HTTP-запросов.
  • Spring Data JPA. Понадобится для работы с базами данных (мы разберем это в следующей статье).
  • PostgreSQL Driver. Драйвер для подключения к базе данных PostgreSQL.
  • После нажатия кнопки «Generate» вы получите архив. Распакуйте его и откройте папку проекта в IntelliJ IDEA.

    !Структура стандартного Spring Boot проекта с ключевыми директориями и файлами конфигурации.

    Разбор конфигурации сборки (Gradle Kotlin DSL)

    Сердцем вашего проекта является файл build.gradle.kts. Если вы раньше использовали Maven (файл pom.xml) или Gradle на Groovy, то синтаксис Kotlin DSL покажется вам более строгим, но и более понятным.

    Давайте разберем ключевые блоки этого файла.

    Плагины

    В блоке plugins подключаются расширения, необходимые для сборки:

    Здесь мы видим плагины для Spring Boot, управления зависимостями и самого языка Kotlin. Обратите внимание на плагины plugin.spring и plugin.jpa. Они автоматически делают классы Kotlin «открытыми» (open) там, где это нужно фреймворку Spring, так как по умолчанию все классы в Kotlin финальные и от них нельзя наследоваться.

    Зависимости

    Блок dependencies описывает библиотеки, которые использует ваш проект:

    * implementation — зависимость нужна для компиляции и запуска. * runtimeOnly — зависимость нужна только во время работы (например, драйвер БД). * testImplementation — зависимость нужна только для тестов.

    Библиотека jackson-module-kotlin критически важна: она позволяет Spring корректно преобразовывать JSON в Kotlin-классы (Data Classes) и обратно, понимая их конструкторы.

    Структура проекта

    Внутри папки src вы найдете две основные директории:

  • main/kotlin: Здесь будет жить весь ваш код. Spring уже создал главный класс приложения, обычно он называется KotlinFullstackApplication.kt (или похоже, в зависимости от имени артефакта).
  • main/resources: Здесь хранятся настройки и статические файлы. Главный файл настроек — application.properties. Я рекомендую сразу переименовать его в application.yml, так как формат YAML более читаем и удобен для иерархических структур.
  • Главный класс приложения выглядит примерно так:

    Аннотация @SpringBootApplication — это магия, которая говорит: «Это точка входа. Просканируй этот пакет и подпакеты, найди все компоненты и запусти сервер».

    Основы развертывания: Docker

    Современная разработка немыслима без контейнеризации. Docker позволяет упаковать ваше приложение вместе со всеми необходимыми ему версиями Java и библиотек в единый образ. Это гарантирует, что если приложение работает у вас на машине, оно будет работать и на сервере.

    Создание Dockerfile

    Создайте файл с именем Dockerfile (без расширения) в корне вашего проекта. Это инструкция для сборки образа.

    Разберем команды: * FROM: указывает, на основе какой операционной системы и версии Java мы строим наш образ. alpine — это очень легкая версия Linux, что уменьшает размер итогового файла. * WORKDIR: устанавливает текущую папку внутри контейнера. * COPY: берет файл с вашего компьютера (хоста) и помещает его в файловую систему образа. * ENTRYPOINT: команда, которая выполнится при старте контейнера.

    Сборка и запуск

    Теперь давайте соберем проект и запустим его в Docker.

  • Сборка JAR-файла.
  • Откройте терминал в папке проекта и выполните команду Gradle: * Windows: .\gradlew clean build * macOS/Linux: ./gradlew clean build

    Эта команда скомпилирует Kotlin-код, прогонит тесты и создаст исполняемый файл в папке build/libs.

  • Сборка Docker-образа.
  • Выполните команду: Флаг -t задает имя (тег) для образа, а точка . в конце указывает, что Dockerfile находится в текущей папке.

  • Запуск контейнера.
  • Флаг -p 8080:8080 «пробрасывает» порт. Это значит, что порт 8080 на вашем компьютере будет перенаправлять запросы на порт 8080 внутри контейнера.

    Если вы все сделали правильно, в консоли вы увидите логи запуска Spring Boot, и приложение будет доступно по адресу http://localhost:8080.

    !Процесс трансформации исходного кода в работающий Docker-контейнер.

    Заключение

    Мы успешно инициализировали проект, разобрались с build.gradle.kts и подготовили Dockerfile для деплоя. Теперь у нас есть надежная основа для разработки.

    В следующей статье мы перейдем к проектированию данных: создадим сущности (Entities), подключим реальную базу данных PostgreSQL и настроим автоматическое создание таблиц.

    2. Проектирование слоя данных: создание сущностей и подключение базы данных

    Проектирование слоя данных: создание сущностей и подключение базы данных

    Приветствую вас во второй части курса Fullstack-разработка на Kotlin: от настройки до деплоя. В прошлой статье мы подготовили почву: настроили проект, разобрались с Gradle и научились упаковывать приложение в Docker. Но пока наше приложение — это просто пустая оболочка. Ему нечего запоминать и нечего отдавать пользователю.

    Сегодня мы займемся «сердцем» любой информационной системы — данными. Мы подключим настоящую промышленную базу данных PostgreSQL, спроектируем структуру хранения информации с помощью Kotlin-классов и научимся выполнять операции CRUD (Create, Read, Update, Delete) без написания SQL-запросов вручную.

    Подготовка окружения: PostgreSQL в Docker

    Разворачивать базу данных (БД) локально через установщик — это прошлый век. Это засоряет систему и создает проблемы с версиями. Поскольку мы уже знакомы с Docker, давайте запустим PostgreSQL в контейнере.

    Для управления несколькими контейнерами (наше приложение + база данных) идеально подходит инструмент Docker Compose. Создайте файл docker-compose.yml в корне вашего проекта.

    Разберем, что здесь происходит:

  • image: Мы используем официальный образ PostgreSQL версии 16 на базе Alpine Linux (он легкий и быстрый).
  • environment: Задаем имя базы данных, пользователя и пароль. Эти же данные нам понадобятся в конфигурации приложения.
  • ports: Пробрасываем стандартный порт Postgres 5432 на наш локальный компьютер.
  • volumes: Это критически важный момент. Мы создаем именованный том db_data, чтобы данные сохранялись даже после удаления контейнера.
  • Запустите базу данных командой в терминале:

    Флаг -d (detach) означает запуск в фоновом режиме.

    Конфигурация Spring Boot

    Теперь нужно «подружить» наше Kotlin-приложение с запущенной базой. Откройте файл src/main/resources/application.yml (если у вас .properties, переименуйте его в .yml, как мы договаривались ранее).

    Добавьте следующие настройки:

    Ключевые параметры

    * ddl-auto: update. Эта настройка говорит Hibernate (ORM-фреймворку под капотом Spring Data JPA): «Посмотри на мои классы-сущности и, если таблиц в базе нет, создай их. Если они есть, но изменились — обнови их». Важно: В продакшене (production) так делать опасно, там используются инструменты миграции вроде Flyway или Liquibase, но для разработки update подходит идеально. * show-sql: true. В консоли мы будем видеть SQL-запросы, которые генерирует Hibernate. Это полезно для отладки.

    ORM и Сущности (Entities)

    Мы подходим к концепции ORM (Object-Relational Mapping). Это технология, которая связывает объектный мир (классы Kotlin) с реляционным миром (таблицы SQL).

    !Визуализация процесса маппинга: как класс превращается в строку таблицы базы данных.

    В Kotlin создание сущности выглядит лаконично. Создайте пакет model и в нем файл Task.kt.

    Разбор анатомии сущности

  • @Entity: Аннотация говорит Spring, что этот класс нужно сохранять в базе данных.
  • @Table(name = "tasks"): Указывает имя таблицы. Если не задать, таблица будет называться task.
  • @Id и @GeneratedValue: Поле id является первичным ключом. Стратегия IDENTITY означает, что база данных сама будет генерировать уникальный номер (autoincrement).
  • Типы данных: Обратите внимание на val id: Long? = null. При создании нового объекта id еще не существует, он появится только после сохранения в БД. Поэтому он nullable.
  • var vs val: Поля, которые могут меняться (заголовок, статус), мы объявляем как var. Идентификатор и дата создания обычно не меняются, поэтому id и createdAt могут быть val (хотя для JPA часто все делают var, но Kotlin позволяет писать чище).
  • > Многие новички используют data class для сущностей JPA. Это допустимо, но может привести к проблемам с производительностью (из-за реализации hashCode и toString, которые могут случайно подтянуть связанные данные). Для простых сущностей это не критично, но профессиональная рекомендация — использовать обычный class.

    Репозитории: Магия Spring Data

    Как нам теперь сохранить задачу в базу или найти её? Писать SQL INSERT INTO...? Нет. Spring Data JPA предоставляет интерфейс Repository.

    Создайте пакет repository и файл TaskRepository.kt:

    Это всё! Нам не нужно писать реализацию этого интерфейса. Spring создаст прокси-объект на лету.

    Наследуясь от JpaRepository<Task, Long>, мы сразу получаем набор готовых методов: * save(entity) — сохранить или обновить. * findById(id) — найти по ID. * findAll() — получить список всех записей. * deleteById(id) — удалить.

    Более того, Spring умеет парсить имена методов. Метод findAllByIsCompleted автоматически превратится в SQL-запрос вида: SELECT * FROM tasks WHERE is_completed = ?.

    Проверка работы

    Чтобы убедиться, что всё работает, давайте создадим компонент, который выполнит код при старте приложения. Это не часть финального приложения, а способ тестирования.

    В корневом пакете (рядом с main функцией) можно временно добавить такой бин:

    (Не забудьте добавить этот метод внутрь класса KotlinFullstackApplication или создать отдельный класс конфигурации).

    Запустите приложение через IntelliJ IDEA или ./gradlew bootRun. В консоли вы должны увидеть логи Hibernate (создание таблицы) и наши принты.

    Заключение

    Сегодня мы совершили огромный скачок. Мы:

  • Запустили PostgreSQL в Docker.
  • Настроили подключение Spring Boot к базе данных.
  • Спроектировали сущность Task.
  • Создали репозиторий для доступа к данным.
  • Теперь у нас есть надежный слой данных. Но как к нему достучаться из браузера или мобильного приложения? В следующей статье мы займемся созданием REST API: напишем контроллеры, DTO и сервисный слой, чтобы управлять нашими задачами через HTTP-запросы.

    3. Реализация бизнес-логики и управление системой через сервисные методы

    Реализация бизнес-логики и управление системой через сервисные методы

    Добро пожаловать в третью часть курса Fullstack-разработка на Kotlin: от настройки до деплоя. Мы уже проделали большую работу: настроили окружение, упаковали проект в Docker и научились сохранять данные в PostgreSQL. Однако, на данный момент наше приложение похоже на библиотеку без библиотекаря: книги (данные) есть, полки (база данных) есть, но нет никого, кто мог бы выдать книгу, принять новую или проверить читательский билет.

    Сегодня мы создадим «мозг» нашего приложения — Сервисный слой. Мы разберем, почему нельзя обращаться к базе данных напрямую из контроллеров, зачем нужны DTO (Data Transfer Objects) и как управлять транзакциями в Spring Boot, используя элегантность языка Kotlin.

    Архитектура приложения: зачем нужен Service?

    В разработке на Spring Boot принята трехслойная архитектура. Это стандарт индустрии, который обеспечивает гибкость и тестируемость кода.

    !Трехслойная архитектура: Контроллер принимает запросы, Сервис обрабатывает логику, Репозиторий работает с данными.

  • Controller (Контроллер): Отвечает за общение с внешним миром. Он принимает HTTP-запросы и возвращает ответы. В нем не должно быть бизнес-логики.
  • Service (Сервис): Здесь живет бизнес-логика. Валидация данных, сложные вычисления, вызовы сторонних API, управление транзакциями — всё это происходит здесь.
  • Repository (Репозиторий): Отвечает только за сохранение и извлечение данных из базы. Он ничего не знает о бизнесе.
  • Почему это важно? Представьте, что завтра вы захотите изменить способ уведомления пользователя о новой задаче (с email на SMS). Если логика размазана по контроллеру, вам придется переписывать код, отвечающий за HTTP. Если она в сервисе — вы меняете только одну функцию, не затрагивая API.

    Паттерн DTO: защита данных

    Прежде чем писать сервис, нам нужно решить проблему передачи данных. В прошлой статье мы создали сущность Task. Но использовать сущности базы данных (Entity) напрямую в API — плохая практика.

    Почему нельзя возвращать Entity наружу: * Безопасность: В сущности могут быть поля, которые нельзя показывать (пароли, внутренние ID, системные флаги). * Связность: Если вы измените структуру таблицы в БД, это автоматически сломает API для всех клиентов (фронтенда, мобильного приложения). * Циклические ссылки: При сложных связях (JSON) сериализатор может уйти в бесконечный цикл.

    Решение — DTO (Data Transfer Object). Это простые классы, предназначенные только для передачи данных.

    Создайте пакет dto и добавьте в него два файла.

    TaskRequest.kt

    Этот класс мы будем использовать при создании или обновлении задачи.

    TaskResponse.kt

    Этот класс мы будем отдавать клиенту.

    Мапперы (Mappers)

    Нам нужно научить приложение превращать Task в TaskResponse и наоборот. В Kotlin для этого идеально подходят функции-расширения (Extension Functions). Создайте файл Extensions.kt в пакете util или mapper:

    Теперь у любой задачи Task есть метод .toDto(). Это делает код невероятно чистым.

    Создание сервиса TaskService

    Теперь перейдем к реализации логики. Создайте пакет service и класс TaskService.

    Обратите внимание на аннотацию @Service. Она говорит Spring: «Создай экземпляр этого класса и храни его в контексте». Также мы используем Constructor Injection (внедрение через конструктор). В Kotlin это делается прямо в заголовке класса: class TaskService(private val taskRepository: TaskRepository). Это самый надежный способ внедрения зависимостей.

    Реализация метода создания (Create)

    Здесь мы видим четкий поток данных: запрос -> сущность -> база -> сущность -> ответ.

    Реализация метода чтения (Read)

    Получение одной задачи по ID требует обработки ситуации, когда задача не найдена.

    Реализация удаления (Delete)

    Магия транзакций: @Transactional

    Вы могли заметить аннотацию @Transactional над методом updateTask. Что она делает?

    Транзакция — это последовательность действий, которая должна быть выполнена полностью или не выполнена вовсе (принцип атомарности).

    Представьте, что в методе обновления мы сначала меняем заголовок, а потом пытаемся отправить уведомление по почте, и отправка падает с ошибкой. Без транзакции заголовок в базе изменится, а уведомление не уйдет. Данные станут несогласованными.

    С аннотацией @Transactional:

  • Spring открывает транзакцию перед началом метода.
  • Если метод завершается успешно, транзакция фиксируется (Commit).
  • Если вылетает исключение (Exception), все изменения в базе данных откатываются (Rollback), как будто их и не было.
  • В методе createTask и deleteTask транзакция тоже полезна, хотя для одной операции save или delete Spring Data открывает транзакцию автоматически. Однако хорошей практикой считается явно помечать методы, меняющие данные, как транзакционные.

    Обработка ошибок

    Сейчас наш сервис выбрасывает EntityNotFoundException. Это стандартное исключение JPA. Чтобы оно не «роняло» сервер с 500 ошибкой, а возвращало понятный ответ, нам понадобится глобальный обработчик ошибок. Но это тема следующей статьи про контроллеры.

    Пока что нам важно понимать: Сервис не должен возвращать HTTP-коды (200, 404, 500). Сервис должен возвращать данные или выбрасывать исключения. Решать, какой HTTP-код отдать клиенту — задача контроллера.

    Итоговый код сервиса

    Вот как выглядит наш полный класс TaskService:

    Заключение

    Мы реализовали надежный слой бизнес-логики. Наше приложение теперь умеет:

  • Принимать безопасные DTO.
  • Конвертировать их в сущности базы данных.
  • Выполнять операции CRUD.
  • Гарантировать целостность данных с помощью транзакций.
  • Но пока что вызвать эти методы можно только из кода. В следующей статье мы откроем наше приложение миру: создадим REST Controller, настроим эндпоинты (Endpoints) и сможем управлять задачами через HTTP-запросы, используя Postman или браузер.

    4. Разработка REST API: создание контроллеров и маршрутизация эндпоинтов

    Разработка REST API: создание контроллеров и маршрутизация эндпоинтов

    Добро пожаловать в четвертую часть курса Fullstack-разработка на Kotlin: от настройки до деплоя. Мы прошли долгий путь: настроили Docker, спроектировали базу данных и написали надежный сервисный слой с бизнес-логикой. Но сейчас наше приложение похоже на гениального ученого, запертого в бункере без связи с внешним миром. Оно умеет работать, но никто не может отправить ему команду.

    Сегодня мы откроем двери. Мы создадим REST API — интерфейс, через который наш будущий фронтенд (и любые другие клиенты) будут общаться с сервером. Мы разберем, как правильно принимать HTTP-запросы, маршрутизировать их и отправлять корректные ответы, используя всю мощь Spring Boot и лаконичность Kotlin.

    Роль контроллера в архитектуре MVC

    В архитектуре Spring Boot контроллер (Controller) играет роль администратора или официанта. Он не готовит еду (это делает Сервис), он не закупает продукты (это делает Репозиторий). Его задача — принять заказ от клиента, проверить, что заказ понятен, передать его на кухню и вернуть готовое блюдо клиенту.

    !Поток обработки запроса: от клиента через контроллер и сервис к базе данных и обратно.

    Контроллер должен быть «тонким». Это значит, что в нем не должно быть сложной логики, циклов обработки данных или транзакций. Он занимается только HTTP-транспортом:

  • Слушает определенный URL (эндпоинт).
  • Извлекает данные из запроса (тело JSON, параметры пути).
  • Вызывает метод сервиса.
  • Формирует HTTP-ответ (статус код + данные).
  • Создание REST-контроллера

    В Spring Framework есть две похожие аннотации: @Controller и @RestController. Первая используется для классических MVC-приложений, где сервер отдает HTML-страницы. Нам же нужно отдавать «сырые» данные в формате JSON, поэтому мы будем использовать @RestController.

    Создайте пакет controller и в нем класс TaskController.kt.

    Разберем аннотации: * @RestController: Говорит Spring, что этот класс обрабатывает запросы и ответы автоматически сериализуются в JSON. * @RequestMapping("/api/tasks"): Задает базовый префикс для всех методов внутри класса. Все наши эндпоинты будут начинаться с этого адреса.

    Обратите внимание на конструктор: class TaskController(private val taskService: TaskService). Мы внедряем наш сервис, созданный в прошлом уроке, чтобы делегировать ему работу.

    Реализация CRUD эндпоинтов

    Теперь давайте реализуем основные операции: получение, создание, обновление и удаление задач.

    Получение данных (GET)

    Для чтения данных используется HTTP-метод GET. В Spring ему соответствует аннотация @GetMapping.

    * getAllTasks сработает на запрос GET /api/tasks. Spring автоматически превратит список объектов TaskResponse в JSON-массив. * getTaskById сработает на запрос вида GET /api/tasks/1, GET /api/tasks/42 и т.д. Аннотация @PathVariable берет значение из фигурных скобок {id} в URL и передает его в аргумент функции.

    Создание данных (POST)

    Для создания новых ресурсов используется метод POST. Данные обычно передаются в теле запроса (Body) в формате JSON.

    Аннотация @RequestBody критически важна. Она говорит Spring: «Возьми JSON из тела запроса и попытайся превратить его в объект TaskRequest». Если JSON не совпадет со структурой класса, Spring выбросит ошибку.

    Обновление данных (PUT)

    Для полного обновления ресурса используется PUT.

    Здесь мы комбинируем @PathVariable (чтобы знать, какую задачу обновляем) и @RequestBody (чтобы знать, какие новые данные записать).

    Удаление данных (DELETE)

    Управление HTTP-статусами: ResponseEntity

    Код выше работает, но он не идеален с точки зрения протокола HTTP. По умолчанию, если метод возвращает данные, Spring ставит статус 200 OK. Но: * При успешном создании принято возвращать 201 Created. * При успешном удалении, когда тело ответа пустое, принято возвращать 204 No Content.

    Чтобы управлять этим, мы можем возвращать не просто объект, а обертку ResponseEntity.

    Давайте улучшим наши методы:

    Теперь наш API ведет себя профессионально и предсказуемо для клиентов.

    Глобальная обработка ошибок

    В прошлой статье в сервисе мы писали такой код: .orElseThrow { EntityNotFoundException("Task not found") }

    Если мы запустим приложение сейчас и запросим несуществующий ID, сервер вернет статус 500 Internal Server Error и огромный стек-трейс ошибки. Это плохо, потому что:

  • 500 означает «сервер сломался», а не «клиент ошибся».
  • Стек-трейс раскрывает внутренности системы, что небезопасно.
  • Нам нужно перехватить это исключение и вернуть красивый 404 Not Found.

    В Spring Boot для этого есть механизм @ControllerAdvice. Создайте пакет exception и класс GlobalExceptionHandler.kt.

    Как это работает:

  • @ControllerAdvice сканирует все контроллеры.
  • @ExceptionHandler(EntityNotFoundException::class) говорит: «Если где-то вылетит это исключение, не падай, а вызови этот метод».
  • Мы формируем JSON вида {"error": "Task not found"} и отдаем его со статусом 404.
  • Настройка CORS

    Мы создаем Fullstack-приложение. Это значит, что наш фронтенд (например, на React) будет запускаться на одном порту (скажем, 3000), а бэкенд — на другом (8080). Браузеры по умолчанию блокируют такие запросы в целях безопасности. Это называется CORS (Cross-Origin Resource Sharing).

    Чтобы разрешить фронтенду обращаться к бэкенду, нужно добавить аннотацию @CrossOrigin над контроллером.

    Теперь браузер позволит нашему будущему фронтенд-приложению получать данные.

    Проверка работы

    На данном этапе у нас нет фронтенда, но мы можем проверить API с помощью инструментов разработчика:

  • Postman или Insomnia — графические клиенты для тестирования API.
  • cURL — утилита командной строки.
  • IntelliJ IDEA HTTP Client — встроенный инструмент (файлы с расширением .http).
  • Пример запроса на создание задачи (POST): URL: http://localhost:8080/api/tasks Body (JSON):

    Если вы все сделали правильно, в ответ придет JSON с созданной задачей, у которой уже будет id и createdAt.

    Заключение

    Мы превратили наш набор классов в полноценный веб-сервер. Теперь наше приложение умеет:

  • Слушать HTTP-запросы.
  • Понимать JSON.
  • Маршрутизировать запросы к нужным методам бизнес-логики.
  • Возвращать правильные HTTP-статусы.
  • Красиво обрабатывать ошибки.
  • В следующей статье мы покинем уютный мир Kotlin и бэкенда, чтобы создать клиентскую часть приложения. Мы подключим современный фронтенд-фреймворк и научим его отображать наши задачи в браузере.

    5. Интеграция с фронтендом и взаимодействие клиент-серверной архитектуры

    Интеграция с фронтендом и взаимодействие клиент-серверной архитектуры

    Добро пожаловать в пятую часть курса Fullstack-разработка на Kotlin: от настройки до деплоя. Мы уже построили мощный бэкенд: у нас есть база данных, бизнес-логика и REST API, готовый отвечать на запросы. Но пока что нашим единственным «пользователем» был Postman или терминал.

    Сегодня мы сделаем наше приложение полноценным. Мы создадим клиентскую часть (Frontend), которая будет работать в браузере, и научим её общаться с нашим сервером. Поскольку наш курс посвящен Kotlin, мы воспользуемся технологией Kotlin/JS вместе с библиотекой React. Это позволит нам писать фронтенд на том же языке, что и бэкенд, используя знакомую типизацию и синтаксис.

    Архитектура взаимодействия

    Прежде чем писать код, важно понять, как именно фронтенд и бэкенд будут работать вместе. В современной веб-разработке используется модель Single Page Application (SPA).

    !Визуализация потока данных между клиентом, сервером и базой данных.

  • Клиент (Браузер): Загружает статические файлы (HTML, JS, CSS) один раз. Затем он отправляет асинхронные запросы за данными.
  • API (Сервер): Принимает запросы, обрабатывает их и возвращает только данные в формате JSON (не HTML-страницы).
  • Рендеринг: Браузер получает JSON и на его основе перерисовывает часть экрана.
  • Настройка Kotlin/JS проекта

    Для фронтенда мы создадим отдельный модуль или проект. В мире Kotlin/JS сборка также осуществляется через Gradle. Нам понадобятся специальные плагины и обертки (Wrappers) для React.

    В файле build.gradle.kts фронтенд-модуля ключевыми зависимостями будут:

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

    Огромное преимущество использования Kotlin на обеих сторонах — возможность переиспользования кода. В идеальном мире мы бы вынесли DTO (Data Transfer Objects) в общий модуль (shared). Но для простоты текущего урока мы просто продублируем структуру данных, чтобы понять принцип.

    Создадим файл Task.kt на клиенте:

    Обратите внимание на аннотацию @Serializable. Она необходима для автоматического преобразования JSON, пришедшего с сервера, в объект Kotlin.

    Клиент API: общение с сервером

    Не стоит писать вызовы fetch прямо внутри компонентов React. Лучшая практика — вынести логику общения с сетью в отдельный объект или класс. Это наш API Layer.

    Мы будем использовать Ktor Client для JS, так как он поддерживает корутины (Coroutines), что делает асинхронный код чистым и понятным.

    Здесь мы видим ключевое слово suspend. В JavaScript нет потоков, но есть Promise. Корутины Kotlin/JS под капотом превращаются в Promises, позволяя нам писать код так, будто он выполняется последовательно, без «ада колбэков» (callback hell).

    Создание React-компонента

    Теперь самое интересное: отображение данных. В Kotlin-React мы используем DSL (предметно-ориентированный язык) для описания HTML.

    Создадим компонент App.kt:

    Разбор кода компонента

  • useState: Это хук React. Он создает переменную tasks, при изменении которой React автоматически перерисует компонент.
  • useEffectOnce: Код внутри этого блока выполнится только один раз — когда компонент появится на странице. Это идеальное место для первичной загрузки данных.
  • scope.launch: Поскольку Api.getTasks() — это suspend функция, мы не можем вызвать её просто так. Нам нужно запустить корутину.
  • HTML DSL: Вместо тегов <div /> мы используем функции div {}. Знак + перед строкой означает добавление текстового узла.
  • Проблема CORS и её решение

    Если вы запустите фронтенд (обычно на порту 3000) и бэкенд (на порту 8080) и попытаетесь сделать запрос, браузер выдаст ошибку в консоли:

    > Access to fetch at 'http://localhost:8080/api/tasks' from origin 'http://localhost:3000' has been blocked by CORS policy.

    Это механизм безопасности браузера. Он запрещает скриптам с одного сайта (origin) обращаться к другому сайту без явного разрешения.

    В предыдущей статье мы уже добавили аннотацию @CrossOrigin в наш контроллер:

    Эта аннотация добавляет в ответ сервера специальный заголовок Access-Control-Allow-Origin: http://localhost:3000, который говорит браузеру: «Всё в порядке, этому сайту можно доверять мои данные».

    Обработка ошибок на клиенте

    Сеть ненадежна. Сервер может упасть, интернет может пропасть. Хороший фронтенд должен быть готов к этому.

    Модифицируем наш вызов API, добавив блок try-catch:

    Заключение

    Мы успешно интегрировали фронтенд на Kotlin/JS с нашим бэкендом на Spring Boot. Теперь у нас есть работающее Fullstack-приложение, где:

  • Бэкенд управляет данными и безопасностью.
  • Фронтенд обеспечивает удобный интерфейс для пользователя.
  • API служит мостом между ними.
  • Использование Kotlin на обоих концах провода снижает когнитивную нагрузку: вы используете одни и те же коллекции, типы данных и логику построения кода.

    В следующей, заключительной статье курса, мы разберем, как подготовить наше приложение к выходу в большой мир: настроим production-сборку и развернем всё это на облачном сервере.