🌐 Архитектурный стиль API
🔗

RESTful API — Полное руководство

REST (Representational State Transfer) — архитектурный стиль для создания веб-сервисов. Простой, масштабируемый, понятный. Стандарт индустрии.

GET POST PUT PATCH DELETE

🌐 Что такое REST

REST (Representational State Transfer) — это архитектурный стиль для создания веб-сервисов, предложенный Роем Филдингом в 2000 году. Это не протокол и не стандарт, а набор принципов проектирования API.

🎯 Простыми словами

REST API — это способ общения между клиентом и сервером через HTTP.

Клиент (браузер, мобильное приложение) отправляет HTTP-запросы.
Сервер обрабатывает их и возвращает данные (обычно JSON).

Это как меню в ресторане: вы делаете заказ (запрос), официант (HTTP) передаёт его на кухню (сервер), и вам приносят блюдо (ответ).

Почему REST так популярен?

  • Простота — использует стандартный HTTP, знакомый всем
  • Универсальность — работает с любым клиентом (web, mobile, IoT)
  • Масштабируемость — stateless архитектура легко масштабируется
  • Кешируемость — HTTP-кеширование из коробки
  • Читаемость — URL понятны человеку
💡 RESTful vs REST

REST — архитектурный стиль (теория).
RESTful — API, которое следует принципам REST (практика).

Если ваш API соблюдает все принципы REST — он RESTful.

📐 Принципы REST

REST определяет 6 ключевых принципов (constraints). API считается RESTful, если соблюдает их все:

1
Client-Server
Клиент и сервер разделены. Клиент не знает, как хранятся данные. Сервер не знает, как отображается UI. Они общаются только через API.
2
Stateless
Сервер не хранит состояние клиента между запросами. Каждый запрос содержит всю информацию для его обработки (токен, параметры).
3
Cacheable
Ответы должны явно указывать, можно ли их кешировать. Это снижает нагрузку на сервер и ускоряет клиент.
4
Uniform Interface
Единый интерфейс для всех ресурсов. Стандартные HTTP-методы, понятные URL, консистентный формат данных.
5
Layered System
Клиент не знает, общается ли он напрямую с сервером или через прокси/балансировщик. Слои независимы.
6
Code on Demand (опц.)
Сервер может отправлять исполняемый код клиенту (JavaScript). Единственный опциональный принцип.

📮 HTTP методы (CRUD)

REST использует стандартные HTTP-методы для операций над ресурсами. Это соответствует CRUD-операциям:

Метод CRUD Описание Идемпотентность
GET Read Получить ресурс(ы). Не изменяет данные. ✅ Да
POST Create Создать новый ресурс. ❌ Нет
PUT Update Полностью заменить ресурс. ✅ Да
PATCH Update Частично обновить ресурс. ❌ Нет*
DELETE Delete Удалить ресурс. ✅ Да
🔄 Что такое идемпотентность?

Идемпотентный метод — повторный вызов даёт тот же результат.

DELETE /users/1 — первый раз удалит, второй вернёт 404. Результат один — пользователя нет.

POST /users — каждый вызов создаёт нового пользователя. Не идемпотентен.

📊 HTTP статус-коды

Статус-код показывает результат запроса. Используйте правильные коды — это часть контракта вашего API:

2xx Успех
  • 200OK — успешный GET/PUT/PATCH
  • 201Created — ресурс создан (POST)
  • 204No Content — успех без тела (DELETE)
3xx Редиректы
  • 301Moved Permanently
  • 302Found (временный)
  • 304Not Modified (кеш)
4xx Ошибки клиента
  • 400Bad Request — неверные данные
  • 401Unauthorized — не авторизован
  • 403Forbidden — нет прав
  • 404Not Found — не найдено
  • 409Conflict — конфликт данных
  • 422Unprocessable — ошибка валидации
  • 429Too Many Requests — rate limit
5xx Ошибки сервера
  • 500Internal Server Error
  • 502Bad Gateway
  • 503Service Unavailable
  • 504Gateway Timeout

🔗 Дизайн URL (эндпоинтов)

URL в REST — это адреса ресурсов (существительные), а не действий (глаголы). Действие определяется HTTP-методом.

📚 Пример: API для управления книгами
GET /api/v1/books Получить список всех книг
GET /api/v1/books/123 Получить книгу с ID=123
POST /api/v1/books Создать новую книгу
PUT /api/v1/books/123 Полностью обновить книгу
PATCH /api/v1/books/123 Частично обновить книгу
DELETE /api/v1/books/123 Удалить книгу

Вложенные ресурсы

👥 Пользователи и их заказы
GET /api/v1/users/5/orders Заказы пользователя #5
GET /api/v1/users/5/orders/42 Заказ #42 пользователя #5
POST /api/v1/users/5/orders Создать заказ для пользователя

Фильтрация, сортировка, пагинация

🔍 Query параметры
GET /api/v1/books?author=Толстой Фильтр по автору
GET /api/v1/books?sort=-created_at Сортировка (- = DESC)
GET /api/v1/books?page=2&limit=20 Пагинация
GET /api/v1/books?fields=id,title Выбор полей
⚠️ Антипаттерны URL

❌ Глаголы в URL: /getBooks, /createUser
❌ Действия в URL: /books/delete/123
❌ Единственное число: /book/123 (используйте множественное)
❌ CamelCase: /userOrders (используйте kebab-case или snake_case)

📤 Запросы и ответы

Структура запроса

HTTP Request
POST /api/v1/users HTTP/1.1 Host: api.example.com Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIs... { "name": "John Doe", "email": "john@example.com", "password": "secret123" }

Структура ответа (успех)

201 Created POST /api/v1/users
{
"status": "success",
"data": {
    "id": 42,
    "name": "John Doe",
    "email": "john@example.com",
    "created_at": "2024-01-15T10:30:00Z"
}
}

Структура ответа (ошибка)

422 Unprocessable Entity POST /api/v1/users
{
"status": "error",
"message": "Validation failed",
"errors": [
    {
        "field": "email",
        "message": "Email already exists"
    },
    {
        "field": "password",
        "message": "Password must be at least 8 characters"
    }
]
}

Ответ списка с пагинацией

200 OK GET /api/v1/users?page=2&limit=10
{
"status": "success",
"data": [
    { "id": 11, "name": "User 11" },
    { "id": 12, "name": "User 12" },
        // ... ещё 8 записей
    ],
    "meta": {
        "current_page": 2,
        "per_page": 10,
        "total_pages": 5,
        "total_count": 47
    },
    "links": {
        "self": "/api/v1/users?page=2&limit=10",
        "first": "/api/v1/users?page=1&limit=10",
        "prev": "/api/v1/users?page=1&limit=10",
        "next": "/api/v1/users?page=3&limit=10",
        "last": "/api/v1/users?page=5&limit=10"
    }
}

🌶️ Пример: Flask REST API

Flask — минималистичный Python-фреймворк. Отлично подходит для понимания основ REST API.

Terminal — Установка
$ pip install flask
Python app.py — Полный CRUD API
from flask import Flask, jsonify, request, abort
from datetime import datetime

app = Flask(__name__)

# ═══════════════════════════════════════════════════════════════
#                    ПРОСТАЯ "БАЗА ДАННЫХ"
# ═══════════════════════════════════════════════════════════════

# В реальном проекте это будет PostgreSQL, MongoDB и т.д.
books_db = {
    1: {
        "id": 1,
        "title": "Война и мир",
        "author": "Лев Толстой",
        "year": 1869,
        "created_at": "2024-01-01T10:00:00Z"
    },
    2: {
        "id": 2,
        "title": "Преступление и наказание",
        "author": "Фёдор Достоевский",
        "year": 1866,
        "created_at": "2024-01-02T12:00:00Z"
    }
}
next_id = 3

# ═══════════════════════════════════════════════════════════════
#                     ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ═══════════════════════════════════════════════════════════════

def success_response(data, status_code=200):
    """Формирует успешный ответ"""
    return jsonify({
        "status": "success",
        "data": data
    }), status_code

def error_response(message, status_code, errors=None):
    """Формирует ответ с ошибкой"""
    response = {
        "status": "error",
        "message": message
    }
    if errors:
        response["errors"] = errors
    return jsonify(response), status_code

def validate_book(data, partial=False):
    """Валидация данных книги"""
    errors = []

    if not partial:
        if not data.get("title"):
            errors.append({"field": "title", "message": "Title is required"})
        if not data.get("author"):
            errors.append({"field": "author", "message": "Author is required"})

    if "year" in data:
        if not isinstance(data["year"], int) or data["year"] < 0:
            errors.append({"field": "year", "message": "Year must be a positive integer"})

    return errors

# ═══════════════════════════════════════════════════════════════
#                        REST ENDPOINTS
# ═══════════════════════════════════════════════════════════════

# ─────────────── GET /books — Получить все книги ───────────────
@app.route('/api/v1/books', methods=['GET'])
def get_books():
    """
    Получить список всех книг

    Query параметры:
        - author: фильтр по автору
        - year: фильтр по году
        - sort: сортировка (title, -title, year, -year)
    """
    books = list(books_db.values())

    # Фильтрация
    author = request.args.get('author')
    if author:
        books = [b for b in books if author.lower() in b['author'].lower()]

    year = request.args.get('year', type=int)
    if year:
        books = [b for b in books if b['year'] == year]

    # Сортировка
    sort = request.args.get('sort', 'id')
    reverse = sort.startswith('-')
    sort_field = sort.lstrip('-')
    if sort_field in ['id', 'title', 'year']:
        books = sorted(books, key=lambda x: x.get(sort_field, ''), reverse=reverse)

    return success_response(books)

# ─────────────── GET /books/:id — Получить одну книгу ───────────────
@app.route('/api/v1/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
    """Получить книгу по ID"""
    book = books_db.get(book_id)

    if not book:
        return error_response(f"Book with id {book_id} not found", 404)

    return success_response(book)

# ─────────────── POST /books — Создать книгу ───────────────
@app.route('/api/v1/books', methods=['POST'])
def create_book():
    """Создать новую книгу"""
    global next_id

    # Проверяем Content-Type
    if not request.is_json:
        return error_response("Content-Type must be application/json", 415)

    data = request.get_json()

    # Валидация
    errors = validate_book(data)
    if errors:
        return error_response("Validation failed", 422, errors)

    # Создаём книгу
    book = {
        "id": next_id,
        "title": data["title"],
        "author": data["author"],
        "year": data.get("year"),
        "created_at": datetime.utcnow().isoformat() + "Z"
    }

    books_db[next_id] = book
    next_id += 1

    return success_response(book, 201)

# ─────────────── PUT /books/:id — Полное обновление ───────────────
@app.route('/api/v1/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
    """Полностью заменить книгу"""
    if book_id not in books_db:
        return error_response(f"Book with id {book_id} not found", 404)

    if not request.is_json:
        return error_response("Content-Type must be application/json", 415)

    data = request.get_json()

    # Валидация (все поля обязательны для PUT)
    errors = validate_book(data, partial=False)
    if errors:
        return error_response("Validation failed", 422, errors)

    # Обновляем (полная замена, но сохраняем id и created_at)
    old_book = books_db[book_id]
    books_db[book_id] = {
        "id": book_id,
        "title": data["title"],
        "author": data["author"],
        "year": data.get("year"),
        "created_at": old_book["created_at"],
        "updated_at": datetime.utcnow().isoformat() + "Z"
    }

    return success_response(books_db[book_id])

# ─────────────── PATCH /books/:id — Частичное обновление ───────────────
@app.route('/api/v1/books/<int:book_id>', methods=['PATCH'])
def patch_book(book_id):
    """Частично обновить книгу"""
    if book_id not in books_db:
        return error_response(f"Book with id {book_id} not found", 404)

    if not request.is_json:
        return error_response("Content-Type must be application/json", 415)

    data = request.get_json()

    # Валидация (частичная)
    errors = validate_book(data, partial=True)
    if errors:
        return error_response("Validation failed", 422, errors)

    # Обновляем только переданные поля
    book = books_db[book_id]
    for field in ['title', 'author', 'year']:
        if field in data:
            book[field] = data[field]

    book["updated_at"] = datetime.utcnow().isoformat() + "Z"

    return success_response(book)

# ─────────────── DELETE /books/:id — Удалить книгу ───────────────
@app.route('/api/v1/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
    """Удалить книгу"""
    if book_id not in books_db:
        return error_response(f"Book with id {book_id} not found", 404)

    del books_db[book_id]

    # 204 No Content — стандарт для успешного DELETE
    return '', 204

# ═══════════════════════════════════════════════════════════════
#                     ОБРАБОТКА ОШИБОК
# ═══════════════════════════════════════════════════════════════

@app.errorhandler(404)
def not_found(error):
    return error_response("Resource not found", 404)

@app.errorhandler(500)
def internal_error(error):
    return error_response("Internal server error", 500)

# ═══════════════════════════════════════════════════════════════
#                          ЗАПУСК
# ═══════════════════════════════════════════════════════════════

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Тестирование Flask API

Terminal — cURL тесты
# Запустите сервер
$ python app.py
# GET — Получить все книги
$ curl http://localhost:5000/api/v1/books
# GET — Получить одну книгу
$ curl http://localhost:5000/api/v1/books/1
# POST — Создать книгу
$ curl -X POST http://localhost:5000/api/v1/books \
-H "Content-Type: application/json" \ -d '{"title": "Мастер и Маргарита", "author": "Михаил Булгаков", "year": 1967}'
# PATCH — Обновить год
$ curl -X PATCH http://localhost:5000/api/v1/books/1 \
-H "Content-Type: application/json" \ -d '{"year": 1869}'
# DELETE — Удалить книгу
$ curl -X DELETE http://localhost:5000/api/v1/books/2

Пример: FastAPI (современный подход)

FastAPI — современный Python-фреймворк с автоматической валидацией, документацией и поддержкой async. Рекомендуется для новых проектов.

🌶️ Flask
  • Простой, минимум магии
  • Много документации
  • Большая экосистема
  • ⚠️ Валидация вручную
  • ⚠️ Нет async из коробки
  • ⚠️ Swagger вручную
FastAPI
  • Автоматическая валидация
  • Swagger/OpenAPI из коробки
  • Async поддержка
  • Type hints
  • Очень быстрый
  • ⚠️ Моложе, меньше плагинов
Terminal — Установка
$ pip install fastapi uvicorn
Python main.py — FastAPI CRUD
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
from enum import Enum

app = FastAPI(
    title="Books API",
    description="REST API для управления книгами",
    version="1.0.0"
)

# ═══════════════════════════════════════════════════════════════
#                    PYDANTIC МОДЕЛИ (СХЕМЫ)
# ═══════════════════════════════════════════════════════════════

class BookBase(BaseModel):
    """Базовые поля книги"""
    title: str = Field(..., min_length=1, max_length=200, example="Война и мир")
    author: str = Field(..., min_length=1, max_length=100, example="Лев Толстой")
    year: Optional[int] = Field(None, ge=0, le=2100, example=1869)

class BookCreate(BookBase):
    """Схема для создания книги"""
    pass

class BookUpdate(BaseModel):
    """Схема для частичного обновления"""
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    author: Optional[str] = Field(None, min_length=1, max_length=100)
    year: Optional[int] = Field(None, ge=0, le=2100)

class BookResponse(BookBase):
    """Схема ответа с книгой"""
    id: int
    created_at: datetime
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True

class SortOrder(str, Enum):
    """Порядок сортировки"""
    asc = "asc"
    desc = "desc"

# ═══════════════════════════════════════════════════════════════
#                    ПРОСТАЯ "БАЗА ДАННЫХ"
# ═══════════════════════════════════════════════════════════════

books_db: dict[int, dict] = {
    1: {
        "id": 1,
        "title": "Война и мир",
        "author": "Лев Толстой",
        "year": 1869,
        "created_at": datetime(2024, 1, 1, 10, 0),
        "updated_at": None
    },
    2: {
        "id": 2,
        "title": "Преступление и наказание",
        "author": "Фёдор Достоевский",
        "year": 1866,
        "created_at": datetime(2024, 1, 2, 12, 0),
        "updated_at": None
    }
}
next_id = 3

# ═══════════════════════════════════════════════════════════════
#                        REST ENDPOINTS
# ═══════════════════════════════════════════════════════════════

@app.get("/api/v1/books", response_model=List[BookResponse], tags=["Books"])
async def get_books(
    author: Optional[str] = Query(None, description="Фильтр по автору"),
    year: Optional[int] = Query(None, description="Фильтр по году"),
    sort_by: str = Query("id", enum=["id", "title", "year"]),
    order: SortOrder = Query(SortOrder.asc)
):
    """Получить список всех книг с фильтрацией и сортировкой"""
    books = list(books_db.values())

    # Фильтрация
    if author:
        books = [b for b in books if author.lower() in b["author"].lower()]
    if year:
        books = [b for b in books if b["year"] == year]

    # Сортировка
    reverse = order == SortOrder.desc
    books = sorted(books, key=lambda x: x.get(sort_by) or "", reverse=reverse)

    return books

@app.get("/api/v1/books/{book_id}", response_model=BookResponse, tags=["Books"])
async def get_book(book_id: int):
    """Получить книгу по ID"""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book {book_id} not found")
    return books_db[book_id]

@app.post("/api/v1/books", response_model=BookResponse, status_code=201, tags=["Books"])
async def create_book(book: BookCreate):
    """Создать новую книгу"""
    global next_id

    new_book = {
        "id": next_id,
        **book.model_dump(),
        "created_at": datetime.utcnow(),
        "updated_at": None
    }

    books_db[next_id] = new_book
    next_id += 1

    return new_book

@app.put("/api/v1/books/{book_id}", response_model=BookResponse, tags=["Books"])
async def update_book(book_id: int, book: BookCreate):
    """Полностью обновить книгу"""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book {book_id} not found")

    old_book = books_db[book_id]
    books_db[book_id] = {
        "id": book_id,
        **book.model_dump(),
        "created_at": old_book["created_at"],
        "updated_at": datetime.utcnow()
    }

    return books_db[book_id]

@app.patch("/api/v1/books/{book_id}", response_model=BookResponse, tags=["Books"])
async def patch_book(book_id: int, book: BookUpdate):
    """Частично обновить книгу"""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book {book_id} not found")

    # Обновляем только переданные поля
    update_data = book.model_dump(exclude_unset=True)
    for field, value in update_data.items():
        books_db[book_id][field] = value

    books_db[book_id]["updated_at"] = datetime.utcnow()

    return books_db[book_id]

@app.delete("/api/v1/books/{book_id}", status_code=204, tags=["Books"])
async def delete_book(book_id: int):
    """Удалить книгу"""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book {book_id} not found")

    del books_db[book_id]
    return None

# ═══════════════════════════════════════════════════════════════
#                         HEALTH CHECK
# ═══════════════════════════════════════════════════════════════

@app.get("/health", tags=["System"])
async def health_check():
    """Проверка работоспособности API"""
    return {"status": "healthy", "timestamp": datetime.utcnow()}
Terminal — Запуск FastAPI
# Запуск сервера
$ uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Started reloader process
# Автоматическая документация (Swagger UI)
Откройте: http://127.0.0.1:8000/docs
# Альтернативная документация (ReDoc)
Откройте: http://127.0.0.1:8000/redoc
🎉 Магия FastAPI

FastAPI автоматически генерирует:

Swagger UI — интерактивная документация на /docs
OpenAPI схему — на /openapi.json
Валидацию — из Pydantic моделей и type hints
Сериализацию — автоматическое преобразование в JSON

Best Practices

🔗
Используйте существительные в URL
✅ GET /api/v1/users
✅ GET /api/v1/users/123/orders
❌ GET /api/v1/getUsers
❌ POST /api/v1/createUser
📦
Версионируйте API
✅ /api/v1/users
✅ /api/v2/users
✅ Header: API-Version: 2
❌ /api/users (без версии)
📊
Правильные статус-коды
✅ 201 для POST (создано)
✅ 204 для DELETE (нет контента)
✅ 404 если не найдено
❌ 200 для всего
📄
Консистентный формат ответа
✅ {"status": "success", "data": {...}}
✅ {"status": "error", "message": "..."}
❌ Разный формат для разных эндпоинтов
📑
Пагинация для списков
✅ ?page=2&limit=20
✅ ?offset=20&limit=10
✅ ?cursor=abc123
❌ Возвращать 10000 записей
🔍
Фильтрация через query
✅ GET /users?status=active
✅ GET /users?created_after=2024-01-01
❌ POST /users/filter с телом
🔐
Аутентификация
✅ Authorization: Bearer <token>
✅ JWT токены
✅ OAuth 2.0
❌ Токен в URL: ?token=abc
📝
Документация
✅ OpenAPI / Swagger
✅ Примеры запросов/ответов
✅ Описание ошибок
❌ Без документации
⚠️ Частые ошибки

❌ Глаголы в URL: /getUser, /deleteBook
❌ Игнорирование статус-кодов: всегда возвращать 200
❌ Передача секретов в URL: /api?password=123
❌ Нет валидации: принимать любые данные
❌ Разные форматы: camelCase и snake_case вперемешку
❌ Огромные ответы: возвращать все поля всегда

FAQ

REST:
• Простой, понятный, стандартный
• Хорошее кеширование через HTTP
• Подходит для большинства CRUD-приложений

GraphQL:
• Гибкие запросы (клиент выбирает поля)
• Один эндпоинт для всего
• Сложнее кеширование
• Подходит для сложных связанных данных

Совет: Начните с REST. Переходите на GraphQL, если появятся проблемы с over-fetching/under-fetching.
PUT — полная замена ресурса:
Нужно отправить ВСЕ поля. Если поле не отправлено — оно станет null.

PATCH — частичное обновление:
Отправляете только те поля, которые хотите изменить.

Пример:
PUT /users/1 с {"name": "John"} — email станет null
PATCH /users/1 с {"name": "John"} — изменится только name

На практике: PATCH используется чаще, он удобнее.
Популярные подходы:

1. JWT (JSON Web Tokens):
• Токен в заголовке: Authorization: Bearer <token>
• Stateless — сервер не хранит сессии
• Подходит для микросервисов

2. OAuth 2.0:
• Для интеграции с внешними сервисами
• "Войти через Google/GitHub"

3. API Keys:
• Для server-to-server
• Заголовок: X-API-Key: <key>
Способы версионирования:

1. В URL (рекомендуется):
/api/v1/users, /api/v2/users
Плюсы: явно, просто, кешируется

2. В заголовке:
Accept: application/vnd.api+json;version=2
Плюсы: чистые URL

3. Query параметр:
/api/users?version=2
Редко используется

Совет: Используйте версию в URL — это самый понятный способ.
Правила обработки ошибок:

1. Правильный статус-код: 400, 401, 403, 404, 422, 500...

2. Понятное сообщение:
{"error": "User not found"} — не {"error": "Error"}

3. Детали для валидации:
{"errors": [{"field": "email", "message": "Invalid format"}]}

4. Не показывайте внутренности:
Скрывайте stack traces и SQL-ошибки в продакшене.
Для начинающих: Flask
• Меньше магии, всё понятно
• Больше туториалов и ресурсов

Для новых проектов: FastAPI
• Современный, быстрый
• Автоматическая документация
• Автоматическая валидация
• Type hints

Совет: Изучите Flask для понимания основ, затем переходите на FastAPI для реальных проектов.
Инструменты для тестирования:

1. Postman / Insomnia:
GUI для ручного тестирования запросов

2. cURL:
Командная строка: curl -X GET http://api.example.com/users

3. HTTPie:
Удобнее cURL: http GET api.example.com/users

4. pytest + requests:
Автоматические тесты на Python

5. Встроенные клиенты:
FastAPI TestClient, Flask test_client()
🔗 REST API — Итоги
📐
Архитектура
Client-Server, Stateless, Cacheable
📮
HTTP методы
GET, POST, PUT, PATCH, DELETE
🔗
URL дизайн
Существительные, множественное число
📊
Статус-коды
2xx успех, 4xx клиент, 5xx сервер
🐍
Python фреймворки
Flask (простой), FastAPI (современный)
📝
Документация
OpenAPI / Swagger обязательно

📋 Шпаргалка по REST API

Cheat Sheet REST API Quick Reference
╔═══════════════════════════════════════════════════════════════════════════╗
║                        REST API QUICK REFERENCE                           ║
╚═══════════════════════════════════════════════════════════════════════════╝

┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP МЕТОДЫ                                                                 │
├─────────────────────────────────────────────────────────────────────────────┤
 GET     /resources        → Получить список                              
 GET     /resources/:id    → Получить один                                
 POST    /resources        → Создать новый                                
 PUT     /resources/:id    → Полностью заменить                           
 PATCH   /resources/:id    → Частично обновить                            
 DELETE  /resources/:id    → Удалить                                      
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ СТАТУС-КОДЫ                                                                 │
├─────────────────────────────────────────────────────────────────────────────┤
 200 OK              → GET/PUT/PATCH успешно                              
 201 Created         → POST успешно (ресурс создан)                       
 204 No Content      → DELETE успешно (нет тела)                          
 400 Bad Request     → Неверный запрос                                    
 401 Unauthorized    → Не авторизован                                     
 403 Forbidden       → Нет прав доступа                                   
 404 Not Found       → Ресурс не найден                                   
 422 Unprocessable   → Ошибка валидации                                   
 500 Server Error    → Ошибка сервера                                     
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ ПРИМЕРЫ URL                                                                 │
├─────────────────────────────────────────────────────────────────────────────┤
 /api/v1/users                    → Коллекция пользователей                
 /api/v1/users/123                → Пользователь #123                      
 /api/v1/users/123/orders         → Заказы пользователя #123               
 /api/v1/users?status=active      → Фильтрация                             
 /api/v1/users?sort=-created_at   → Сортировка (- = DESC)                  
 /api/v1/users?page=2&limit=20    → Пагинация                              
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ ЗАГОЛОВКИ                                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
 Content-Type: application/json       → Тип тела запроса                   
 Accept: application/json             → Ожидаемый тип ответа               
 Authorization: Bearer <token>        → JWT аутентификация                 
 X-Request-ID: uuid                   → ID запроса для трейсинга           
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ ФОРМАТ ОТВЕТА                                                               │
├─────────────────────────────────────────────────────────────────────────────┤
                                                                             
  // Успешный ответ                                                         
  {                                                                          
    "status": "success",                                                     
    "data": { ... }                                                          
  }                                                                          
                                                                             
  // Ответ с ошибкой                                                         
  {                                                                          
    "status": "error",                                                       
    "message": "Validation failed",                                          
    "errors": [                                                              
      { "field": "email", "message": "Invalid email" }                       
    ]                                                                        
  }                                                                          
                                                                             
└─────────────────────────────────────────────────────────────────────────────┘

🧪 Пример тестирования API

Python test_api.py — pytest
import pytest
from fastapi.testclient import TestClient
from main import app

# Создаём тестовый клиент
client = TestClient(app)

# ═══════════════════════════════════════════════════════════════
#                     ТЕСТЫ ДЛЯ GET
# ═══════════════════════════════════════════════════════════════

def test_get_all_books():
    """Тест получения всех книг"""
    response = client.get("/api/v1/books")

    assert response.status_code == 200
    assert isinstance(response.json(), list)

def test_get_book_by_id():
    """Тест получения книги по ID"""
    response = client.get("/api/v1/books/1")

    assert response.status_code == 200
    data = response.json()
    assert data["id"] == 1
    assert "title" in data

def test_get_book_not_found():
    """Тест 404 для несуществующей книги"""
    response = client.get("/api/v1/books/99999")

    assert response.status_code == 404

# ═══════════════════════════════════════════════════════════════
#                     ТЕСТЫ ДЛЯ POST
# ═══════════════════════════════════════════════════════════════

def test_create_book():
    """Тест создания книги"""
    new_book = {
        "title": "Тестовая книга",
        "author": "Тестовый автор",
        "year": 2024
    }

    response = client.post("/api/v1/books", json=new_book)

    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Тестовая книга"
    assert "id" in data
    assert "created_at" in data

def test_create_book_validation_error():
    """Тест валидации при создании"""
    invalid_book = {
        "title": "",  # Пустой title
        "author": "Test"
    }

    response = client.post("/api/v1/books", json=invalid_book)

    assert response.status_code == 422  # Unprocessable Entity

# ═══════════════════════════════════════════════════════════════
#                     ТЕСТЫ ДЛЯ PATCH
# ═══════════════════════════════════════════════════════════════

def test_patch_book():
    """Тест частичного обновления"""
    update_data = {"year": 2025}

    response = client.patch("/api/v1/books/1", json=update_data)

    assert response.status_code == 200
    data = response.json()
    assert data["year"] == 2025
    assert data["updated_at"] is not None

# ═══════════════════════════════════════════════════════════════
#                     ТЕСТЫ ДЛЯ DELETE
# ═══════════════════════════════════════════════════════════════

def test_delete_book():
    """Тест удаления книги"""
    # Сначала создаём книгу для удаления
    new_book = {"title": "To Delete", "author": "Test"}
    create_response = client.post("/api/v1/books", json=new_book)
    book_id = create_response.json()["id"]

    # Удаляем
    response = client.delete(f"/api/v1/books/{book_id}")

    assert response.status_code == 204

    # Проверяем, что книга удалена
    get_response = client.get(f"/api/v1/books/{book_id}")
    assert get_response.status_code == 404

# ═══════════════════════════════════════════════════════════════
#                     ЗАПУСК ТЕСТОВ
# ═══════════════════════════════════════════════════════════════

# pytest test_api.py -v
Terminal — Запуск тестов
$ pytest test_api.py -v
test_api.py::test_get_all_books PASSED test_api.py::test_get_book_by_id PASSED test_api.py::test_get_book_not_found PASSED test_api.py::test_create_book PASSED test_api.py::test_create_book_validation_error PASSED test_api.py::test_patch_book PASSED test_api.py::test_delete_book PASSED ========================= 7 passed in 0.42s =========================
🚀 Следующие шаги

1. Создайте свой API — начните с Flask или FastAPI
2. Добавьте базу данных — SQLAlchemy или SQLModel
3. Реализуйте аутентификацию — JWT токены
4. Напишите тесты — pytest + TestClient
5. Задокументируйте — OpenAPI / Swagger
6. Задеплойте — Docker + любой хостинг

REST API — фундаментальный навык для любого разработчика. Понимание REST открывает двери к бэкенд-разработке и интеграциям.

© 2025 • RESTful API Guide

Flask • FastAPI • OpenAPI • HTTP • JSON