Skip to content
IRC-CodingIRC-Coding
REST API основыHTTP методы коды статусаHATEOASRichardson Maturity ModelWeb Services

REST API: HTTP-методы, коды статуса и HATEOAS

REST API основы: HTTP-методы, коды статуса, HATEOAS и Richardson Maturity Model с примерами.

S

schutzgeist

14 min read
REST API: HTTP-методы, коды статуса и HATEOAS

Основы 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: отсутствие состояния на стороне сервера
  • Практическое применение: современная архитектура веб-сервисов

Основные компоненты

  1. Ресурсы: идентифицируемые сущности с URI
  2. HTTP-методы: стандартизированные операции
  3. Статус-коды: единообразное форматирование ответов
  4. Representations: JSON, XML, HTML и прочее
  5. HATEOAS: навигация через гипермедиа
  6. Statelessness: безгосударственная коммуникация
  7. Cacheability: HTTP-заголовки кеширования
  8. 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

Типичные вопросы на экзамене

  1. В чем разница между PUT и PATCH? PUT заменяет весь ресурс, PATCH обновляет только отдельные части.

  2. Объясните HATEOAS! Hypermedia as the Engine of Application State - API самостоятельно обнаруживаются через гиперссылки.

  3. Что означает идемпотентность HTTP-методов? Множественные вызовы имеют такой же эффект, как один вызов.

  4. Какие уровни есть в модели зрелости Ричардсона? Уровень 0 (POX), Уровень 1 (Resources), Уровень 2 (HTTP Verbs), Уровень 3 (Hypermedia).

Основные источники

  1. https://restfulapi.net/
  2. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
  3. https://martinfowler.com/articles/richardsonMaturityModel.html
Назад к блогу
Share:

Nächster Artikel in Веб-разработка

Weiterlesen
Добавляем llms.txt в Astro-блог

Похожие статьи