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 выглядит со стороны Java-кода
- Первый REST API на Spring Boot
- Проверяем API через curl
- Типовая ошибка: POST без Content-Type
- Как API ведет себя на плохих данных
- REST-клиент на Java: HttpClient и RestClient
- Что еще нужно API перед production
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Зачем он нужен: через 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 — не обязательно.



