REST в Java: что это, зачем нужен и как написать первый API на Spring Boot

REST в Java: что это, зачем нужен и как написать первый API на Spring Boot Полезное

REST в Java — это не часть языка и не библиотека, а способ построить HTTP API по правилам архитектурного стиля REST: данные представлены ресурсами с адресами (/api/books/1), действия над ними выражены методами HTTP (GET, POST, DELETE), результат — кодом ответа (200, 201, 404), а данные обычно передаются в JSON. Java дает для этого два набора инструментов: серверные фреймворки (Spring Boot, Jakarta REST) и HTTP-клиенты (HttpClient, RestClient, RestTemplate).

Зачем он нужен: через REST API мобильное приложение, фронтенд на React или другой сервис получают данные Java-бэкенда, не зная, как он устроен внутри. Ниже — короткий словарь, рабочий API на Spring Boot, проверка через curl, типовые ошибки и клиент на Java.

Все примеры проверены 24.09.2026 на Java 25 (Eclipse Temurin), Spring Boot 4.1.1 и Maven 3.9.16.

Мини-словарь: что с чем не путать

Термин Что это
REST архитектурный стиль (набор ограничений), описанный Роем Филдингом в 2000 году; не протокол и не формат
REST API HTTP API, построенный по этим ограничениям (на практике часто частично)
Spring MVC / Spring Boot Spring MVC принимает HTTP-запросы и вызывает ваши методы; Spring Boot собирает его вместе со встроенным сервером Tomcat и настройками
Jakarta REST (JAX-RS) стандартный API Java для REST-сервисов (аннотации @Path, @GET); реализации — Jersey, RESTEasy
JSON текстовый формат данных; REST его не требует, но в Java API это самый частый выбор

Сами ограничения REST (stateless, кэшируемость, единый интерфейс и другие) подробно разобраны в статье Архитектура REST API: принципы, методы HTTP и коды ответов. Здесь — только то, что нужно для кода.

Как REST выглядит со стороны Java-кода

Ресурс «книги» получает адрес /api/books, а каждому сочетанию «метод + адрес» соответствует метод Java-класса.

Запрос Что делает Метод контроллера Успешный код
GET /api/books список книг all() 200
GET /api/books/1 одна книга one(1) 200 или 404
POST /api/books + JSON создать книгу create(...) 201 + заголовок Location
DELETE /api/books/1 удалить книгу delete(1) 204 или 404

Фреймворк сам разбирает URL, превращает JSON из тела запроса в Java-объект, а возвращаемый объект — обратно в JSON. Ваша задача — описать ресурсы, правила проверки и коды ответов.

Первый REST API на Spring Boot

Проект удобно сгенерировать на start.spring.io (Maven, Java 25, зависимости Spring Web и Validation) или в IntelliJ IDEA через New Project -> Spring Boot. В Spring Boot 4 веб-стартер называется spring-boot-starter-webmvc; старое имя spring-boot-starter-web еще собирается, но помечено устаревшим, и в старых туториалах встречается именно оно. Итоговый pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>books</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>25</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Точка входа — src/main/java/com/example/books/BooksApplication.java:

package com.example.books;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BooksApplication {
    public static void main(String[] args) {
        SpringApplication.run(BooksApplication.class, args);
    }
}

Сам REST-контроллер — BookController.java в том же пакете:

package com.example.books;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.net.URI;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/books")
public class BookController {

    // Что клиент присылает в POST: без id, с проверкой полей
    public record NewBook(@NotBlank @Size(max = 200) String title,
                          @NotBlank @Size(max = 100) String author) {}

    // Что сервер хранит и отдает: id назначает сервер
    public record Book(long id, String title, String author) {}

    private final Map<Long, Book> books = new ConcurrentHashMap<>();
    private final AtomicLong ids = new AtomicLong();

    @GetMapping
    public List<Book> all() {
        return books.values().stream()
                .sorted(Comparator.comparingLong(Book::id))
                .toList();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> one(@PathVariable long id) {
        Book book = books.get(id);
        return book == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(book);
    }

    @PostMapping
    public ResponseEntity<Book> create(@Valid @RequestBody NewBook req) {
        long id = ids.incrementAndGet();
        Book book = new Book(id, req.title().strip(), req.author().strip());
        books.put(id, book);
        return ResponseEntity.created(URI.create("/api/books/" + id)).body(book);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable long id) {
        return books.remove(id) == null
                ? ResponseEntity.notFound().build()
                : ResponseEntity.noContent().build();
    }
}

Сборка и запуск: mvn package, затем java -jar target/books-0.0.1-SNAPSHOT.jar. В логе появится строка Tomcat started on port 8080 (http) — API готов принимать запросы. Книги хранятся в памяти: после перезапуска список снова пуст.

Что делает каждая аннотация

Аннотация Роль
@RestController класс обрабатывает HTTP-запросы, а возвращаемые объекты пишутся в тело ответа как JSON
@RequestMapping("/api/books") общий префикс адреса для всех методов класса
@GetMapping, @PostMapping, @DeleteMapping связывают метод HTTP и путь с методом Java
@PathVariable берет значение из адреса: /api/books/7 -> id = 7
@RequestBody превращает JSON из тела запроса в объект NewBook
@Valid + @NotBlank, @Size проверяют поля до вызова метода; при нарушении — ответ 400

Разделение NewBook и Book не формальность: клиент не должен сам назначать id. Если прислать "id": 999 в теле POST, поле будет проигнорировано, и сервер выдаст свой номер.

Проверяем API через curl

curl -i -X POST http://localhost:8080/api/books \
  -H 'Content-Type: application/json' \
  -d '{"title":"Java. Базовый курс","author":"Иванов"}'
curl -s http://localhost:8080/api/books
curl -i http://localhost:8080/api/books/42

Ответы (заголовки Date и Transfer-Encoding опущены):

HTTP/1.1 201
Location: /api/books/1
Content-Type: application/json

{"id":1,"title":"Java. Базовый курс","author":"Иванов"}

[{"id":1,"title":"Java. Базовый курс","author":"Иванов"}]

HTTP/1.1 404
Content-Length: 0

Создание вернуло 201 и адрес новой книги в Location, чтение несуществующей — 404 без тела. Вместо curl подойдет Postman или HTTP-клиент IntelliJ IDEA — важны те же метод, адрес, заголовок и тело.

Типовая ошибка: POST без Content-Type

Неверно — тело есть, а заголовка нет:

curl -i -X POST http://localhost:8080/api/books -d '{"title":"X","author":"Y"}'

Фактический результат:

HTTP/1.1 415
Accept: application/json, application/*+json
Content-Type: application/json

{"timestamp":"2026-09-24T12:42:13.847Z","status":415,"error":"Unsupported Media Type","path":"/api/books"}

Причина: с ключом -d curl сам ставит Content-Type: application/x-www-form-urlencoded, а метод с @RequestBody ждет JSON. В логе сервера это видно прямо: Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported. Исправление — явно указать -H 'Content-Type: application/json', как в примере выше.

Как API ведет себя на плохих данных

Проверка ввода — обязательная часть REST API: сервер не должен падать с 500 или сохранять мусор. Результаты прогона того же контроллера:

Запрос Ответ Почему
{"title":" ","author":"Y"} 400 @NotBlank отклоняет строку из пробелов
title длиной 201 символ 400 @Size(max = 200)
{"title":["a"],"author":"Y"} 400 массив нельзя превратить в строку
тело null 400 «Required request body is missing»
GET /api/books/abc 400 abc не преобразуется в long
{"title":123,"author":"Y"} 201, "title":"123" число молча приводится к строке
{"title":true,"author":"Y"} 201, "title":"true" логическое значение тоже приводится к строке

Две последние строки — граница этого примера: Jackson 3 (JSON-библиотека Spring Boot 4) по умолчанию приводит скаляры к строке. Если число в строковом поле должно считаться ошибкой, это настраивается отдельно в конфигурации Jackson. Тело ответа 400 здесь короткое, без имени поля; подробности пишутся в лог (MethodArgumentNotValidException), а для клиента обычно добавляют обработчик @RestControllerAdvice с понятным сообщением.

REST-клиент на Java: HttpClient и RestClient

Второй сценарий «REST в Java» — вызвать чужой API из своего кода. Встроенный в JDK (с Java 11) java.net.http.HttpClient не требует зависимостей:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class BooksClient {
    public static void main(String[] args) throws Exception {
        String base = "http://localhost:8080/api/books";
        HttpClient http = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(5))
                .build();

        // POST: создаем ресурс, тело - JSON
        String json = """
                {"title": "Чистый код", "author": "Мартин"}
                """;
        HttpRequest post = HttpRequest.newBuilder(URI.create(base))
                .timeout(Duration.ofSeconds(10))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();
        HttpResponse<String> created = http.send(post, HttpResponse.BodyHandlers.ofString());
        System.out.println(created.statusCode() + " " + created.headers().firstValue("Location").orElse("-"));
        System.out.println(created.body());

        // GET: читаем коллекцию
        HttpRequest get = HttpRequest.newBuilder(URI.create(base))
                .timeout(Duration.ofSeconds(10))
                .header("Accept", "application/json")
                .GET()
                .build();
        HttpResponse<String> list = http.send(get, HttpResponse.BodyHandlers.ofString());
        if (list.statusCode() != 200) {
            throw new IllegalStateException("Ожидали 200, получили " + list.statusCode());
        }
        System.out.println(list.body());
    }
}

Запуск одной командой java BooksClient.java при работающем сервере (с пустым списком) печатает:

201 /api/books/1
{"id":1,"title":"Чистый код","author":"Мартин"}
[{"id":1,"title":"Чистый код","author":"Мартин"}]

HttpClient не бросает исключение на 404 или 500 — код ответа нужно проверять самому, как в блоке GET. Таймауты тоже стоит задавать явно. JSON здесь собран строкой ради краткости; в реальном коде объект сериализуют библиотекой (Jackson), чтобы не сломать разметку кавычкой в названии.

Внутри Spring-приложения удобнее RestClient (Spring Framework 6.1+): он сам превращает объекты в JSON и обратно:

RestClient client = RestClient.create("http://localhost:8080");

BookController.Book created = client.post()
        .uri("/api/books")
        .contentType(MediaType.APPLICATION_JSON)
        .body(new BookController.NewBook("Effective Java", "Блох"))
        .retrieve()
        .body(BookController.Book.class);

В прогоне этот вызов вернул Book[id=2, title=Effective Java, author=Блох]. В отличие от HttpClient, retrieve() на 4xx бросает исключение: запрос /api/books/42 дал HttpClientErrorException$NotFound: 404 Not Found.

Клиент Где взять Когда выбирать
HttpClient JDK 11+ без фреймворка, утилиты, минимум зависимостей
RestClient Spring Framework 6.1+ новый код в Spring-приложении
RestTemplate Spring, давно только поддержка существующего кода: в Spring Framework 7.0 пометки deprecated еще нет, но команда Spring объявила ее в 7.1 (план — ноябрь 2026) и удаление в 8.0; новый код пишут на RestClient
WebClient модуль spring-webflux реактивные приложения

Что еще нужно API перед production

Учебный контроллер показывает механику, но не готов к реальной нагрузке. Для рабочего сервиса добавляют: хранение в базе данных вместо Map; аутентификацию и права (обычно Spring Security), иначе удалить книгу может любой; единый формат ошибок (ProblemDetail, RFC 9457); HTTPS; лимиты на размер тела и частоту запросов; журналирование без паролей и персональных данных; тесты контроллера (MockMvc); описание API в OpenAPI.

Если не получилось

Симптом Что проверить
release version 25 not supported при сборке Maven запущен на старом JDK: java -version и mvn -v должны показывать 25
Port 8080 was already in use порт занят; остановите другой процесс или задайте server.port=8081 в application.properties
404 на любой адрес контроллер вне пакета com.example.books или его подпакетов — Spring его не находит
415 на POST нет заголовка Content-Type: application/json
400 на POST пустое или слишком длинное поле, неверный JSON; причина — в логе сервера
Connection refused у клиента сервер не запущен или другой порт/хост

Выводы

  • REST в Java — это HTTP API по правилам стиля REST, реализованный фреймворком: ресурсы — адреса, действия — методы HTTP, результат — коды ответа.
  • В Spring Boot REST API строится из @RestController и @GetMapping/@PostMapping; JSON в объекты и обратно Spring превращает сам.
  • Отдельный класс для входных данных и @Valid защищают от чужого id и пустых полей; злые входы дают 400, а не 500.
  • POST с JSON требует заголовка Content-Type: application/json, иначе 415.
  • Для вызова чужого API есть HttpClient (JDK) и RestClient (Spring); первый не бросает исключений на 4xx/5xx, второй бросает.

Где применяется / связь с практикой

Освойте тему на практике

REST API — основной способ, которым Java-бэкенд отдает данные веб- и мобильным клиентам и общается с другими сервисами. Следующие шаги после этого примера — хранение в базе через Spring Data, Spring Security, тесты и микросервисы; их системно проходят на курсе «Разработчик на Spring Framework». Разобрать отдельные темы с преподавателями можно на бесплатных открытых уроках Otus.

FAQ

Можно ли сделать REST API на Java без Spring?
Да: есть стандарт Jakarta REST с реализациями Jersey и RESTEasy, а также легкие фреймворки вроде Javalin. Spring Boot чаще встречается в вакансиях, но принцип «ресурс — метод HTTP — код ответа» везде один.

Обязательно ли REST API отдает JSON?
Нет, REST не привязан к формату: можно отдавать XML или другой тип, который клиент запросил заголовком Accept. JSON выбирают по умолчанию из-за компактности и поддержки в браузерах.

Чем PUT отличается от PATCH?
PUT заменяет ресурс целиком присланным представлением, PATCH меняет только переданные поля. PUT по спецификации HTTP идемпотентен, PATCH — не обязательно.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)