Практика 7. Практическая работа 7. Версионирование API и структура проекта
Цель
- Научиться разбивать FastAPI-приложение на модули вместо одного
main.py. - Освоить
APIRouterи подключение роутеров черезinclude_router. - Реализовать версионирование API через префикс URL (
/api/v1) и через заголовок. - Организовать проект по слоям:
routers/,schemas/,services/. - Сгруппировать эндпойнты в документации с помощью тегов (
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 Optionalfrom uuid import UUIDfrom 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, Optionalfrom uuid import UUID, uuid4from 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 UUIDfrom typing import Listfrom fastapi import APIRouter, HTTPException, statusfrom app.schemas.book import Book, BookCreatefrom 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, FastAPIfrom 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, Headerfrom 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.
Вопросы для самопроверки
- За что отвечает каждый слой:
schemas/,services/,routers/? Почему роутер не должен напрямую работать с хранилищем? - Что такое
APIRouterи чемprefixотличается от теговtags? - Как подключить роутер к приложению и как задать общий префикс версии для группы роутеров?
- Назовите плюсы и минусы URI-версионирования и версионирования через заголовок.
- Где в коде задаётся путь
/api/v1/books, если префикс/booksобъявлен в роутере, а/api/v1— на уровне сборки? - Зачем нужны теги, и как они влияют на Swagger UI?
- Как получить значение HTTP-заголовка в эндпойнте FastAPI и задать значение по умолчанию?