Основы REST API: HTTP-методы, статус-коды, HATEOAS и Richardson Maturity Model
Это полное руководство по основам REST API, включающее HTTP-методы, статус-коды, HATEOAS и Richardson Maturity Model с практическими примерами.
Краткое резюме
REST API используют HTTP-методы для CRUD-операций, стандартизированные статус-коды для результатов, HATEOAS для самодокументируемых API и соответствуют Richardson Maturity Model для определения уровня зрелости.
Компактное описание
REST (Representational State Transfer) — архитектурный стиль для распределённых систем, использующий HTTP-протокол и стандартные методы.
HTTP-методы:
- GET: запрос ресурса (безопасен, идемпотентен)
- POST: создание ресурса (не безопасен, не идемпотентен)
- PUT: полное обновление или замена ресурса (не безопасен, идемпотентен)
- PATCH: частичное обновление ресурса (не безопасен, не идемпотентен)
- DELETE: удаление ресурса (не безопасен, идемпотентен)
Статус-коды:
- 2xx: успешные операции (200, 201, 204)
- 3xx: редирект (301, 302, 304)
- 4xx: ошибки клиента (400, 401, 403, 404, 422)
- 5xx: ошибки сервера (500, 502, 503)
HATEOAS: Hypermedia as the Engine of Application State, означает что API самодокументируемы благодаря гиперссылкам.
Ключевые моменты
- REST: архитектурный стиль для веб-сервисов с HTTP
- HTTP-методы: GET, POST, PUT, PATCH, DELETE для CRUD
- Статус-коды: стандартизированные коды ответов (2xx, 3xx, 4xx, 5xx)
- HATEOAS: навигация на основе гипермедиа между ресурсами
- Richardson Maturity Model: уровни зрелости REST API (0-3)
- Идемпотентность: несколько вызовов дают одинаковый результат
- Stateless: отсутствие состояния на стороне сервера
- Практическое применение: современная архитектура веб-сервисов
Основные компоненты
- Ресурсы: идентифицируемые сущности с URI
- HTTP-методы: стандартизированные операции
- Статус-коды: единообразное форматирование ответов
- Representations: JSON, XML, HTML и прочее
- HATEOAS: навигация через гипермедиа
- Statelessness: безгосударственная коммуникация
- Cacheability: HTTP-заголовки кеширования
- Uniform Interface: единообразные соглашения API
Практические примеры
1. REST API с Spring Boot (Java)
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// Дата-модель
class User {
private Long id;
private String name;
private String email;
private Map<String, String> _links = new HashMap<>();
public User() {}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters и Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Map<String, String> get_links() { return _links; }
public void set_links(Map<String, String> links) { this._links = links; }
}
// Repository (в памяти для демо)
@Repository
class UserRepository {
private final Map<Long, User> users = new ConcurrentHashMap<>();
private long nextId = 1;
public List<User> findAll() {
return new ArrayList<>(users.values());
}
public Optional<User> findById(Long id) {
return Optional.ofNullable(users.get(id));
}
public User save(User user) {
if (user.getId() == null) {
user.setId(nextId++);
}
users.put(user.getId(), user);
return user;
}
public void deleteById(Long id) {
users.remove(id);
}
public boolean existsById(Long id) {
return users.containsKey(id);
}
}
// REST контроллер
@RestController
@RequestMapping("/api/users")
class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
// GET /api/users - получить всех пользователей
@GetMapping
public ResponseEntity<Map<String, Object>> getAllUsers() {
List<User> users = userRepository.findAll();
// добавить HATEOAS ссылки
for (User user : users) {
addHateoasLinks(user);
}
Map<String, Object> response = new HashMap<>();
response.put("users", users);
response.put("_links", Map.of(
"self", Map.of("href", "/api/users"),
"create", Map.of("href", "/api/users", "method", "POST")
));
response.put("count", users.size());
return ResponseEntity.ok(response);
}
// GET /api/users/{id} - получить пользователя
@GetMapping("/{id}")
public ResponseEntity<?> getUserById(@PathVariable Long id) {
return userRepository.findById(id)
.map(user -> {
addHateoasLinks(user);
return ResponseEntity.ok(user);
})
.orElse(ResponseEntity.notFound().build());
}
// POST /api/users - создать пользователя
@PostMapping
public ResponseEntity<Map<String, Object>> createUser(@RequestBody User user) {
// валидация
if (user.getName() == null || user.getName().trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Имя не может быть пустым"));
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Некорректный адрес электронной почты"));
}
User savedUser = userRepository.save(user);
addHateoasLinks(savedUser);
Map<String, Object> response = new HashMap<>();
response.put("user", savedUser);
response.put("message", "Пользователь успешно создан");
response.put("_links", Map.of(
"self", Map.of("href", "/api/users/" + savedUser.getId()),
"all", Map.of("href", "/api/users")
));
return ResponseEntity
.status(HttpStatus.CREATED)
.body(response);
}
// PUT /api/users/{id} - полное обновление пользователя
@PutMapping("/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
user.setId(id);
User updatedUser = userRepository.save(user);
addHateoasLinks(updatedUser);
Map<String, Object> response = new HashMap<>();
response.put("user", updatedUser);
response.put("message", "Пользователь успешно обновлён");
return ResponseEntity.ok(response);
}
// PATCH /api/users/{id} - частичное обновление пользователя
@PatchMapping("/{id}")
public ResponseEntity<?> partialUpdateUser(@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
return userRepository.findById(id)
.map(user -> {
// обновить только переданные поля
if (updates.containsKey("name")) {
user.setName((String) updates.get("name"));
}
if (updates.containsKey("email")) {
user.setEmail((String) updates.get("email"));
}
User updatedUser = userRepository.save(user);
addHateoasLinks(updatedUser);
Map<String, Object> response = new HashMap<>();
response.put("user", updatedUser);
response.put("message", "Пользователь частично обновлён");
return ResponseEntity.ok(response);
})
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/users/{id} - удалить пользователя
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable Long id) {
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
userRepository.deleteById(id);
Map<String, Object> response = new HashMap<>();
response.put("message", "Пользователь успешно удалён");
response.put("_links", Map.of(
"all", Map.of("href", "/api/users")
));
return ResponseEntity.ok(response);
}
// добавить HATEOAS ссылки
private void addHateoasLinks(User user) {
Map<String, String> links = new HashMap<>();
links.put("self", "/api/users/" + user.getId());
links.put("collection", "/api/users");
links.put("update", "/api/users/" + user.getId());
links.put("delete", "/api/users/" + user.getId());
user.set_links(links);
}
}
// обработчик исключений
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(Map.of("error", e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGenericException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Внутренняя ошибка сервера"));
}
}
// приложение Spring Boot
@SpringBootApplication
public class RestApiApplication {
public static void main(String[] args) {
SpringApplication.run(RestApiApplication.class, args);
}
@Bean
public CommandLineRunner initData(UserRepository userRepository) {
return args -> {
// создать тестовые данные
userRepository.save(new User(null, "Alice", "alice@example.com"));
userRepository.save(new User(null, "Bob", "bob@example.com"));
userRepository.save(new User(null, "Charlie", "charlie@example.com"));
System.out.println("Тестовые данные созданы");
};
}
}
2. Demo модели Richardson Maturity
// Richardson Maturity Model Level 0: Swamp of POX
class Level0Api {
private String baseUrl;
public Level0Api(String baseUrl) {
this.baseUrl = baseUrl;
}
// Никаких HTTP-методов, только POST для всего
public String createUser(String name, String email) {
// POST /api - Без REST-соглашений
String payload = String.format("{\"action\": \"create\", \"name\": \"%s\", \"email\": \"%s\"}",
name, email);
return postRequest("/api", payload);
}
public String getUser(long id) {
// POST /api - Без URI-ресурсов
String payload = String.format("{\"action\": \"get\", \"id\": %d}", id);
return postRequest("/api", payload);
}
private String postRequest(String endpoint, String payload) {
// Симуляция HTTP-вызова
return "POST " + baseUrl + endpoint + " - Body: " + payload;
}
}
// Richardson Maturity Model Level 1: Resources
class Level1Api {
private String baseUrl;
public Level1Api(String baseUrl) {
this.baseUrl = baseUrl;
}
// Отдельные URI для ресурсов, но только GET
public String getUser(long id) {
// GET /api/users/123 - Правильный URI, но только GET
return getRequest("/api/users/" + id);
}
public String getAllUsers() {
return getRequest("/api/users");
}
// Но все остальное через POST
public String createUser(String name, String email) {
String payload = String.format("{\"name\": \"%s\", \"email\": \"%s\"}", name, email);
return postRequest("/api/users", payload);
}
private String getRequest(String endpoint) {
return "GET " + baseUrl + endpoint;
}
private String postRequest(String endpoint, String payload) {
return "POST " + baseUrl + endpoint + " - Body: " + payload;
}
}
// Richardson Maturity Model Level 2: HTTP Verbs
class Level2Api {
private String baseUrl;
public Level2Api(String baseUrl) {
this.baseUrl = baseUrl;
}
// Правильные HTTP-методы для CRUD
public String getUser(long id) {
return "GET " + baseUrl + "/api/users/" + id;
}
public String getAllUsers() {
return "GET " + baseUrl + "/api/users";
}
public String createUser(String name, String email) {
String payload = String.format("{\"name\": \"%s\", \"email\": \"%s\"}", name, email);
return "POST " + baseUrl + "/api/users - Body: " + payload;
}
public String updateUser(long id, String name, String email) {
String payload = String.format("{\"name\": \"%s\", \"email\": \"%s\"}", name, email);
return "PUT " + baseUrl + "/api/users/" + id + " - Body: " + payload;
}
public String deleteUser(long id) {
return "DELETE " + baseUrl + "/api/users/" + id;
}
}
// Richardson Maturity Model Level 3: Hypermedia (HATEOAS)
class Level3Api {
private String baseUrl;
public Level3Api(String baseUrl) {
this.baseUrl = baseUrl;
}
// Полная реализация HATEOAS
public Map<String, Object> getUser(long id) {
Map<String, Object> user = new HashMap<>();
user.put("id", id);
user.put("name", "Alice");
user.put("email", "alice@example.com");
// HATEOAS-ссылки
Map<String, Object> links = new HashMap<>();
links.put("self", Map.of("href", "/api/users/" + id));
links.put("collection", Map.of("href", "/api/users"));
links.put("update", Map.of("href", "/api/users/" + id, "method", "PUT"));
links.put("delete", Map.of("href", "/api/users/" + id, "method", "DELETE"));
user.put("_links", links);
return user;
}
public Map<String, Object> getAllUsers() {
List<Map<String, Object>> users = new ArrayList<>();
// Пользователи со ссылками
Map<String, Object> user1 = getUser(1L);
Map<String, Object> user2 = getUser(2L);
users.add(user1);
users.add(user2);
Map<String, Object> response = new HashMap<>();
response.put("users", users);
// Ссылки коллекции
Map<String, Object> links = new HashMap<>();
links.put("self", Map.of("href", "/api/users"));
links.put("create", Map.of("href", "/api/users", "method", "POST"));
links.put("search", Map.of("href", "/api/users/search", "method", "GET"));
response.put("_links", links);
response.put("count", users.size());
return response;
}
public Map<String, Object> createUser(String name, String email) {
// Возвращаем созданный ресурс со ссылками
Map<String, Object> createdUser = new HashMap<>();
createdUser.put("id", 3L);
createdUser.put("name", name);
createdUser.put("email", email);
Map<String, Object> links = new HashMap<>();
links.put("self", Map.of("href", "/api/users/3"));
links.put("collection", Map.of("href", "/api/users"));
links.put("update", Map.of("href", "/api/users/3", "method", "PUT"));
links.put("delete", Map.of("href", "/api/users/3", "method", "DELETE"));
createdUser.put("_links", links);
Map<String, Object> response = new HashMap<>();
response.put("user", createdUser);
response.put("message", "Пользователь создан");
response.put("_links", Map.of(
"self", Map.of("href", "/api/users/3"),
"all", Map.of("href", "/api/users")
));
return response;
}
}
// Demo модели Richardson Maturity
public class RichardsonMaturityDemo {
public static void main(String[] args) {
System.out.println("=== Demo модели Richardson Maturity ===");
String baseUrl = "http://api.example.com";
// Level 0: Swamp of POX
System.out.println("\n--- Level 0: Swamp of POX ---");
Level0Api level0 = new Level0Api(baseUrl);
System.out.println("Create User: " + level0.createUser("Alice", "alice@example.com"));
System.out.println("Get User: " + level0.getUser(123));
// Level 1: Resources
System.out.println("\n--- Level 1: Resources ---");
Level1Api level1 = new Level1Api(baseUrl);
System.out.println("Get User: " + level1.getUser(123));
System.out.println("Get All Users: " + level1.getAllUsers());
System.out.println("Create User: " + level1.createUser("Bob", "bob@example.com"));
// Level 2: HTTP Verbs
System.out.println("\n--- Level 2: HTTP Verbs ---");
Level2Api level2 = new Level2Api(baseUrl);
System.out.println("Get User: " + level2.getUser(123));
System.out.println("Create User: " + level2.createUser("Charlie", "charlie@example.com"));
System.out.println("Update User: " + level2.updateUser(123, "Charlie Updated", "charlie.new@example.com"));
System.out.println("Delete User: " + level2.deleteUser(123));
// Level 3: Hypermedia (HATEOAS)
System.out.println("\n--- Level 3: Hypermedia (HATEOAS) ---");
Level3Api level3 = new Level3Api(baseUrl);
Map<String, Object> userResponse = level3.getUser(123);
System.out.println("Get User with HATEOAS:");
printJson(userResponse);
Map<String, Object> allUsersResponse = level3.getAllUsers();
System.out.println("\nAll Users with HATEOAS:");
printJson(allUsersResponse);
Map<String, Object> createResponse = level3.createUser("David", "david@example.com");
System.out.println("\nCreate User with HATEOAS:");
printJson(createResponse);
// Richardson Maturity Analysis
System.out.println("\n=== Анализ зрелости Richardson ===");
analyzeMaturityLevel();
}
private static void printJson(Map<String, Object> data) {
System.out.println(jsonToString(data, 0));
}
private static String jsonToString(Object obj, int indent) {
if (obj instanceof Map) {
StringBuilder sb = new StringBuilder();
Map<?, ?> map = (Map<?, ?>) obj;
String indentStr = " ".repeat(indent);
sb.append("{\n");
for (Map.Entry<?, ?> entry : map.entrySet()) {
sb.append(indentStr).append("\"").append(entry.getKey()).append("\": ");
sb.append(jsonToString(entry.getValue(), indent + 1));
sb.append(",\n");
}
if (!map.isEmpty()) {
sb.setLength(sb.length() - 2); // Remove last comma and newline
sb.append("\n");
}
sb.append(" ".repeat(indent - 1)).append("}");
return sb.toString();
} else if (obj instanceof List) {
StringBuilder sb = new StringBuilder();
List<?> list = (List<?>) obj;
String indentStr = " ".repeat(indent);
sb.append("[\n");
for (Object item : list) {
sb.append(indentStr).append(jsonToString(item, indent + 1));
sb.append(",\n");
}
if (!list.isEmpty()) {
sb.setLength(sb.length() - 2);
sb.append("\n");
}
sb.append(" ".repeat(indent - 1)).append("]");
return sb.toString();
} else {
return "\"" + obj + "\"";
}
}
private static void analyzeMaturityLevel() {
System.out.println("Уровни модели Richardson Maturity:");
System.out.println("Level 0 - Swamp of POX: Только HTTP, без REST-соглашений");
System.out.println("Level 1 - Resources: Отдельные URI, но только GET");
System.out.println("Level 2 - HTTP Verbs: Правильные HTTP-методы");
System.out.println("Level 3 - Hypermedia: HATEOAS для API, поддерживающих обнаружение");
System.out.println("\nПреимущества более высоких уровней:");
System.out.println("- Лучшая кешируемость");
System.out.println("- Ясная семантика");
System.out.println("- Слабая связанность между клиентом и сервером");
System.out.println("- Self-describing API");
}
}
3. JavaScript-клиент для REST API
// REST API Client с поддержкой HATEOAS
class RestClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
// Универсальный метод запроса
async request(method, endpoint, data = null) {
const config = {
method: method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
if (data) {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(this.baseUrl + endpoint, config);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
// CRUD-операции
async getAllUsers() {
return this.request('GET', '/users');
}
async getUser(id) {
return this.request('GET', `/users/${id}`);
}
async createUser(userData) {
return this.request('POST', '/users', userData);
}
async updateUser(id, userData) {
return this.request('PUT', `/users/${id}`, userData);
}
async partialUpdateUser(id, updates) {
return this.request('PATCH', `/users/${id}`, updates);
}
async deleteUser(id) {
return this.request('DELETE', `/users/${id}`);
}
// Навигация по HATEOAS
async followLink(resource, linkName) {
const links = resource._links || {};
const link = links[linkName];
if (!link) {
throw new Error(`Link '${linkName}' не найден`);
}
const href = link.href;
const method = link.method || 'GET';
// Обработка абсолютных URL
const url = href.startsWith('http') ? href : this.baseUrl + href;
const config = {
method: method,
headers: {
'Accept': 'application/json'
}
};
const response = await fetch(url, config);
return response.json();
}
// Самообнаруживаемый API-клиент
async discoverApi() {
try {
const root = await this.request('GET', '/');
console.log('API-обнаружение:', root);
return root;
} catch (error) {
console.warn('Ошибка при обнаружении API:', error);
return null;
}
}
}
// Клиент с поддержкой HATEOAS
class HateoasClient {
constructor(baseUrl) {
this.restClient = new RestClient(baseUrl);
this.cache = new Map();
}
async getUserWithNavigation(id) {
const user = await this.restClient.getUser(id);
console.log('Пользователь:', user);
// Показать доступные действия
if (user._links) {
console.log('Доступные действия:');
Object.keys(user._links).forEach(linkName => {
const link = user._links[linkName];
console.log(` ${linkName}: ${link.href} (${link.method || 'GET'})`);
});
}
return user;
}
async navigateToCollection(resource) {
try {
const collection = await this.restClient.followLink(resource, 'collection');
console.log('Коллекция:', collection);
return collection;
} catch (error) {
console.error('Ошибка при переходе к коллекции:', error);
return null;
}
}
async performAction(resource, actionName, data = null) {
try {
const link = resource._links[actionName];
if (!link) {
throw new Error(`Действие '${actionName}' недоступно`);
}
const method = link.method || 'POST';
const endpoint = link.href.replace(this.restClient.baseUrl, '');
return await this.restClient.request(method, endpoint, data);
} catch (error) {
console.error(`Ошибка при выполнении действия '${actionName}':`, error);
throw error;
}
}
}
// Demo REST API
async function restApiDemo() {
console.log('=== REST API Client Demo ===');
const client = new RestClient('http://localhost:8080/api');
const hateoasClient = new HateoasClient('http://localhost:8080/api');
try {
// Обнаружение API
console.log('\n--- Обнаружение API ---');
const apiInfo = await client.discoverApi();
// Получить всех пользователей
console.log('\n--- Все пользователи ---');
const users = await client.getAllUsers();
console.log('Пользователи:', users);
if (users.users && users.users.length > 0) {
const firstUser = users.users[0];
// Пользователь с навигацией HATEOAS
console.log('\n--- Пользователь с HATEOAS ---');
const userWithNav = await hateoasClient.getUserWithNavigation(firstUser.id);
// Переход к коллекции
console.log('\n--- Переход к коллекции ---');
const collection = await hateoasClient.navigateToCollection(userWithNav);
// Обновить пользователя
console.log('\n--- Обновление пользователя ---');
const updatedUser = await client.updateUser(firstUser.id, {
name: 'Updated Name',
email: 'updated@example.com'
});
console.log('Обновленный пользователь:', updatedUser);
}
// Создать нового пользователя
console.log('\n--- Создание пользователя ---');
const newUser = await client.createUser({
name: 'New User',
email: 'newuser@example.com'
});
console.log('Новый пользователь:', newUser);
// Выполнить HATEOAS-действия
if (newUser.user && newUser.user._links) {
console.log('\n--- HATEOAS-действия ---');
// Следовать self-ссылке
const selfUser = await hateoasClient.performAction(newUser.user, 'self');
console.log('Результат self-ссылки:', selfUser);
// Следовать collection-ссылке
const collection = await hateoasClient.performAction(newUser.user, 'collection');
console.log('Результат collection-ссылки:', collection);
}
} catch (error) {
console.error('Ошибка demo:', error);
}
}
// Обработка ошибок и механизм повторных попыток
class RobustRestClient extends RestClient {
constructor(baseUrl, maxRetries = 3) {
super(baseUrl);
this.maxRetries = maxRetries;
}
async requestWithRetry(method, endpoint, data = null) {
let lastError;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await this.request(method, endpoint, data);
} catch (error) {
lastError = error;
// Повторять только при сетевых ошибках или 5xx
if (!this.shouldRetry(error)) {
throw error;
}
const delay = Math.pow(2, attempt) * 1000; // Exponential Backoff
console.warn(`Попытка ${attempt} не удалась, повтор через ${delay}ms:`, error.message);
if (attempt < this.maxRetries) {
await this.sleep(delay);
}
}
}
throw lastError;
}
shouldRetry(error) {
// Повторять при сетевых ошибках или 5xx статус-кодах
return error.message.includes('fetch') ||
error.message.startsWith('HTTP 5');
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Запустить demo
if (typeof module !== 'undefined' && module.exports) {
module.exports = { RestClient, HateoasClient, RobustRestClient, restApiDemo };
} else {
// Браузерная среда
restApiDemo();
}
Обзор HTTP-методов
| Метод | Назначение | Идемпотентный | Безопасный | Примеры |
|---|---|---|---|---|
| GET | Получить ресурс | Да | Да | GET /users |
| POST | Создать ресурс | Нет | Нет | POST /users |
| PUT | Заменить ресурс | Да | Нет | PUT /users/123 |
| PATCH | Обновить ресурс | Нет | Нет | PATCH /users/123 |
| DELETE | Удалить ресурс | Да | Нет | DELETE /users/123 |
Обзор HTTP-статусов
2xx Успешные ответы
- 200 OK: Запрос выполнен успешно
- 201 Created: Ресурс создан
- 204 No Content: Успешно, но нет ответа
3xx Перенаправления
- 301 Moved Permanently: Постоянное перенаправление
- 302 Found: Временное перенаправление
- 304 Not Modified: Не изменено (кэш)
4xx Ошибки клиента
- 400 Bad Request: Некорректный запрос
- 401 Unauthorized: Требуется аутентификация
- 403 Forbidden: Доступ запрещен
- 404 Not Found: Ресурс не найден
- 422 Unprocessable Entity: Ошибка валидации
5xx Ошибки сервера
- 500 Internal Server Error: Ошибка сервера
- 502 Bad Gateway: Ошибка шлюза
- 503 Service Unavailable: Сервис недоступен
Модель зрелости Ричардсона
Уровень 0: Болото POX
- HTTP только как транспортный протокол
- Отсутствуют REST-соглашения
- POST для всех операций
Уровень 1: Ресурсы
- Отдельные URI для каждого ресурса
- Но используется только GET
- HTTP-методы не соответствуют семантике
Уровень 2: HTTP-методы
- Правильные HTTP-методы
- CRUD-операции
- Правильное использование статусов
Уровень 3: Гипермедиа (HATEOAS)
- Все предыдущие уровни
- Гипермедиа-ссылки
- API самоописываются
Лучшие практики HATEOAS
Структура ссылок
{
"_links": {
"self": {
"href": "/api/users/123"
},
"collection": {
"href": "/api/users"
},
"update": {
"href": "/api/users/123",
"method": "PUT"
},
"delete": {
"href": "/api/users/123",
"method": "DELETE"
}
}
}
Встроенные ресурсы
{
"user": {
"id": 123,
"name": "Alice",
"_embedded": {
"orders": [
{
"id": 456,
"total": 99.99
}
]
}
}
}
Рекомендации по проектированию REST API
Проектирование URI
- Существительные: Ресурсы как существительные (/users, /products)
- Множественное число: Коллекции во множественном числе (/users, не /user)
- Иерархия: Логическая структура (/users/123/orders)
- Строчные буквы: Единообразное использование строчных букв
Запросы и ответы
- JSON: Стандартный формат
- Единообразие: Консистентная структура
- Версионирование: Версии API (/api/v1/users)
- Пагинация: Разделение больших наборов данных
Безопасность
- HTTPS: Зашифрованное соединение
- Аутентификация: JWT, OAuth 2.0
- Авторизация: Управление доступом на основе ролей
- Rate Limiting: Защита от злоупотреблений
Преимущества и недостатки
Преимущества REST
- Масштабируемость: Stateless архитектура
- Гибкость: Независимость от платформы
- Кэшируемость: Использование HTTP-кэширования
- Простота: Интуитивные концепции
- Стандартизация: HTTP-стандарты
Недостатки
- Оверхед: Размер HTTP-заголовков
- Stateless: Необходимо управлять состоянием вручную
- Сложность: HATEOAS может быть сложной
- Версионирование: Сложности с версионированием API
Типичные вопросы на экзамене
-
В чем разница между PUT и PATCH? PUT заменяет весь ресурс, PATCH обновляет только отдельные части.
-
Объясните HATEOAS! Hypermedia as the Engine of Application State - API самостоятельно обнаруживаются через гиперссылки.
-
Что означает идемпотентность HTTP-методов? Множественные вызовы имеют такой же эффект, как один вызов.
-
Какие уровни есть в модели зрелости Ричардсона? Уровень 0 (POX), Уровень 1 (Resources), Уровень 2 (HTTP Verbs), Уровень 3 (Hypermedia).
Основные источники
- https://restfulapi.net/
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://martinfowler.com/articles/richardsonMaturityModel.html



