Перейти к содержимому

Практика 7. Практическая работа 7. Версионирование API и структура проекта

Цель

  1. Научиться разбивать FastAPI-приложение на модули вместо одного main.py.
  2. Освоить APIRouter и подключение роутеров через include_router.
  3. Реализовать версионирование API через префикс URL (/api/v1) и через заголовок.
  4. Организовать проект по слоям: routers/, schemas/, services/.
  5. Сгруппировать эндпойнты в документации с помощью тегов (tags).

Теория

Зачем разбивать проект. Когда весь код лежит в main.py, файл разрастается: модели, бизнес-логика и маршруты перемешаны, тяжело искать и тестировать. Решение — разделить ответственность по слоям:

  • schemas/ — Pydantic-модели запросов и ответов (контракт API);
  • services/ — бизнес-логика и доступ к данным (что приложение делает);
  • routers/ — эндпойнты, которые принимают запрос и вызывают сервис (как приложение общается по HTTP);
  • main.py — точка сборки: создаёт FastAPI и подключает роутеры.

APIRouter — «мини-приложение» с собственными маршрутами. Объявляется с prefix (общий префикс пути) и tags (группировка в Swagger), затем подключается к приложению через app.include_router(router).

Версионирование позволяет менять API, не ломая старых клиентов. Два практичных способа:

  • URI-версионирование — версия в пути: /api/v1/books, /api/v2/books. Наглядно, легко тестировать. Удобно вынести каждую версию в свой APIRouter с prefix="/api/v1".
  • Версионирование через заголовок — URI остаётся «чистым» (/api/books), а версия передаётся в заголовке (например, X-API-Version: 2). Соответствует духу REST, но менее очевидно при ручной проверке.

Теги (tags=["books"]) группируют эндпойнты в Swagger UI по разделам, что делает документацию читаемой.


Задание

Шаг 1. Создание структуры проекта

Соберите следующее дерево каталогов (сервис «Книги»):

fastapi-books/
├── app/
│ ├── __init__.py
│ ├── main.py # сборка приложения, include_router
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── book.py # Pydantic-модели
│ ├── services/
│ │ ├── __init__.py
│ │ └── book_service.py # бизнес-логика, хранение в памяти
│ └── routers/
│ ├── __init__.py
│ └── v1/
│ ├── __init__.py
│ └── books.py # эндпойнты версии v1
├── requirements.txt
└── README.md

Установите зависимости: python -m venv .venv, активируйте окружение, затем pip install fastapi uvicorn и pip freeze > requirements.txt.

Шаг 2. Схемы (app/schemas/book.py)

Вынесите Pydantic-модели в отдельный модуль:

from typing import Optional
from uuid import UUID
from pydantic import BaseModel, Field
class BookCreate(BaseModel):
title: str = Field(..., min_length=1, description="Название", examples=["Война и мир"])
author: str = Field(..., min_length=1, description="Автор", examples=["Л.Н. Толстой"])
year: int = Field(..., ge=1400, le=2100, description="Год издания", examples=[1869])
price: float = Field(..., gt=0, description="Цена, > 0")
class Book(BookCreate):
id: UUID

Для частичного обновления (PATCH) добавьте по аналогии модель BookUpdate, где все поля Optional.

Шаг 3. Сервис (app/services/book_service.py)

Перенесите всю работу с данными в сервисный слой. Роутеры не должны знать, как именно хранятся книги:

from typing import List, Optional
from uuid import UUID, uuid4
from app.schemas.book import Book, BookCreate
_DB: dict[UUID, Book] = {}
def list_books() -> List[Book]:
return list(_DB.values())
def get_book(book_id: UUID) -> Optional[Book]:
return _DB.get(book_id)
def create_book(payload: BookCreate) -> Book:
book = Book(id=uuid4(), **payload.model_dump())
_DB[book.id] = book
return book
def delete_book(book_id: UUID) -> bool:
return _DB.pop(book_id, None) is not None

Шаг 4. Роутер версии v1 (app/routers/v1/books.py)

Создайте APIRouter с префиксом /books и тегом books. Эндпойнты только принимают запрос и вызывают сервис:

from uuid import UUID
from typing import List
from fastapi import APIRouter, HTTPException, status
from app.schemas.book import Book, BookCreate
from app.services import book_service
router = APIRouter(prefix="/books", tags=["books"])
@router.get("/", response_model=List[Book], summary="Список книг")
def list_books():
return book_service.list_books()
@router.get("/{book_id}", response_model=Book, summary="Книга по id")
def get_book(book_id: UUID):
book = book_service.get_book(book_id)
if book is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Книга не найдена")
return book
@router.post("/", response_model=Book, status_code=status.HTTP_201_CREATED,
summary="Создать книгу")
def create_book(payload: BookCreate):
return book_service.create_book(payload)
@router.delete("/{book_id}", status_code=status.HTTP_204_NO_CONTENT,
summary="Удалить книгу")
def delete_book(book_id: UUID):
if not book_service.delete_book(book_id):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Книга не найдена")

Шаг 5. Сборка приложения и URI-версионирование (app/main.py)

Подключите роутер books под общим префиксом версии /api/v1. Так все маршруты получают путь вида /api/v1/books:

from fastapi import APIRouter, FastAPI
from app.routers.v1 import books as books_v1
app = FastAPI(title="Books API", version="1.0.0")
# Префикс версии задаём на уровне родительского роутера
api_v1 = APIRouter(prefix="/api/v1")
api_v1.include_router(books_v1.router)
app.include_router(api_v1)
@app.get("/", tags=["root"])
def root():
return {"msg": "Books API. См. /docs"}

Запуск из корня проекта: uvicorn app.main:app --reload. Проверьте http://127.0.0.1:8000/docs — эндпойнты сгруппированы по тегу books, а пути начинаются с /api/v1/books.

Шаг 6 (продвинутое). Версионирование через заголовок

Добавьте обработку версии через заголовок X-API-Version. Создайте зависимость, которая читает заголовок, и эндпойнт, отдающий разный формат:

from fastapi import APIRouter, Header
from typing import Optional
router = APIRouter(prefix="/api/books", tags=["books (header-versioned)"])
@router.get("/")
def list_books(x_api_version: Optional[str] = Header(default="1")):
if x_api_version == "2":
return {"data": [{"id": 1, "full_name": "Война и мир"}]} # формат v2
return [{"id": 1, "title": "Война и мир"}] # формат v1

Подключите роутер через app.include_router(...) и проверьте оба варианта, отправляя запрос с заголовком X-API-Version: 2 и без него.


Критерии оценки

  • 20% — проект разбит на модули schemas/, services/, routers/; структура каталогов соответствует заданию.
  • 20% — эндпойнты вынесены в APIRouter и подключены через include_router; main.py не содержит бизнес-логики.
  • 25% — реализовано URI-версионирование: все маршруты доступны по префиксу /api/v1/....
  • 15% — реализовано версионирование через заголовок X-API-Version (Шаг 6).
  • 10% — эндпойнты сгруппированы тегами; в Swagger UI видны summary/description.
  • 10% — приложение запускается командой uvicorn app.main:app, CRUD работает через /docs.

Вопросы для самопроверки

  1. За что отвечает каждый слой: schemas/, services/, routers/? Почему роутер не должен напрямую работать с хранилищем?
  2. Что такое APIRouter и чем prefix отличается от тегов tags?
  3. Как подключить роутер к приложению и как задать общий префикс версии для группы роутеров?
  4. Назовите плюсы и минусы URI-версионирования и версионирования через заголовок.
  5. Где в коде задаётся путь /api/v1/books, если префикс /books объявлен в роутере, а /api/v1 — на уровне сборки?
  6. Зачем нужны теги, и как они влияют на Swagger UI?
  7. Как получить значение HTTP-заголовка в эндпойнте FastAPI и задать значение по умолчанию?