← Назад к вопросам

Что такое HATEOAS в REST API?

2.7 Senior🔥 151 комментариев
#REST API и HTTP

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# HATEOAS в REST API

Определение

HATEOAS (Hypermedia As The Engine Of Application State) — это архитектурный принцип REST, согласно которому сервер должен возвращать не только данные, но и информацию о доступных действиях над этими данными в виде гиперссылок и операций. Другими словами, клиент должен понимать, что он может делать дальше, исходя только из ответа сервера, без предварительного знания API.

Проблема без HATEOAS

В обычном REST API клиент должен "знать" структуру API заранее:

# Клиент жестко знает, как работает API
response = requests.get("https://api.example.com/users/123")
user = response.json()

# Клиент сам придумывает следующий запрос
# Есть ли у пользователя друзья? Проверим /users/123/friends
# Можно ли отредактировать? Может быть /users/123/edit?
friends_response = requests.get(f"https://api.example.com/users/{user['id']}/friends")

Это создаёт сильную связанность между клиентом и сервером: если сервер изменит URL структуру, клиент сломается.

HATEOAS решение

Сервер явно указывает, какие действия доступны в поле _links:

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": {
      "href": "/api/users/123",
      "method": "GET"
    },
    "edit": {
      "href": "/api/users/123",
      "method": "PUT"
    },
    "delete": {
      "href": "/api/users/123",
      "method": "DELETE"
    },
    "friends": {
      "href": "/api/users/123/friends",
      "method": "GET"
    },
    "posts": {
      "href": "/api/users/123/posts",
      "method": "GET"
    }
  }
}

Теперь клиент может автоматически понять, что ему доступно:

response = requests.get("https://api.example.com/users/123")
user = response.json()

# Клиент ищет нужные ссылки в ответе
if "friends" in user["_links"]:
    friends_response = requests.get(user["_links"]["friends"]["href"])

if "edit" in user["_links"]:
    # Клиент может отредактировать
    edit_url = user["_links"]["edit"]["href"]

Практический пример: Flask с HATEOAS

from flask import Flask, jsonify, request
from functools import wraps

app = Flask(__name__)

posts_db = {
    1: {"id": 1, "title": "First Post", "content": "Hello", "author_id": 1},
    2: {"id": 2, "title": "Second Post", "content": "Python Tips", "author_id": 2},
}

def add_hateoas(post):
    """Добавляет HATEOAS ссылки к посту"""
    post_id = post["id"]
    post["_links"] = {
        "self": {"href": f"/posts/{post_id}", "method": "GET"},
        "edit": {"href": f"/posts/{post_id}", "method": "PUT"},
        "delete": {"href": f"/posts/{post_id}", "method": "DELETE"},
        "author": {"href": f"/users/{post['author_id']}", "method": "GET"},
        "comments": {"href": f"/posts/{post_id}/comments", "method": "GET"},
        "like": {"href": f"/posts/{post_id}/like", "method": "POST"}
    }
    return post

@app.route("/posts", methods=["GET"])
def list_posts():
    """Получить все посты"""
    result = [add_hateoas(p.copy()) for p in posts_db.values()]
    return jsonify({
        "posts": result,
        "_links": {
            "self": {"href": "/posts", "method": "GET"},
            "create": {"href": "/posts", "method": "POST"}
        }
    })

@app.route("/posts/<int:post_id>", methods=["GET"])
def get_post(post_id):
    """Получить конкретный пост"""
    if post_id not in posts_db:
        return jsonify({"error": "Not found"}), 404
    
    post = add_hateoas(posts_db[post_id].copy())
    return jsonify(post)

@app.route("/posts/<int:post_id>", methods=["PUT"])
def update_post(post_id):
    """Обновить пост"""
    if post_id not in posts_db:
        return jsonify({"error": "Not found"}), 404
    
    data = request.json
    posts_db[post_id].update(data)
    post = add_hateoas(posts_db[post_id].copy())
    return jsonify(post)

@app.route("/posts/<int:post_id>", methods=["DELETE"])
def delete_post(post_id):
    """Удалить пост"""
    if post_id not in posts_db:
        return jsonify({"error": "Not found"}), 404
    
    del posts_db[post_id]
    return "", 204

if __name__ == "__main__":
    app.run(debug=True)

Ответ GET /posts/1 будет выглядеть так:

{
  "id": 1,
  "title": "First Post",
  "content": "Hello",
  "author_id": 1,
  "_links": {
    "self": {"href": "/posts/1", "method": "GET"},
    "edit": {"href": "/posts/1", "method": "PUT"},
    "delete": {"href": "/posts/1", "method": "DELETE"},
    "author": {"href": "/users/1", "method": "GET"},
    "comments": {"href": "/posts/1/comments", "method": "GET"},
    "like": {"href": "/posts/1/like", "method": "POST"}
  }
}

Преимущества HATEOAS

  1. Слабая связанность: Клиент не должен знать URL структуру, он следует ссылкам
  2. Эволюция API: Сервер может менять URL без поломки клиентов
  3. Самодокументирование: Ответ показывает, какие действия доступны
  4. Безопасность: Сервер решает, какие ссылки показывать (можно скрыть удаление для неадминов)
  5. Навигация: Клиент может "кликать" по ссылкам вместо хардкода путей

Недостатки HATEOAS

  1. Объём трафика: Каждый ответ содержит дополнительные ссылки
  2. Сложность реализации: Нужно думать о всех возможных действиях
  3. Медленнее в разработке: Требует больше кода на сервере
  4. Мало поддержки в клиентах: Большинство JS клиентов игнорируют HATEOAS

В реальной практике

HATEOAS редко реализуют полностью. Часто используют гибридный подход:

# Более реалистичный вариант
response = {
    "data": user,
    "links": {
        "self": f"/users/{user['id']}",
        "delete": f"/users/{user['id']}"
    }
}

Большинство публичных API (GitHub, Stripe, Twilio) не используют полный HATEOAS, но применяют его отдельные элементы (например, pagination links).

Заключение

HATEOAS — это мощный архитектурный принцип, который делает REST API более гибким и независимым от клиента. Однако его полная реализация редко используется на практике из-за сложности и увеличения объёма данных. В реальных проектах часто используют частичный HATEOAS для критических операций и links в paginated результатах.