Прежде чем что-то улучшать — осознайте что сделано.
За этот курс вы прошли путь от SELECT * FROM products в SQLiteStudio до работающего REST API с ORM, миграциями, Pydantic-схемами и Dependency Injection. Это не "hello world" — это архитектурно правильный бэкенд-проект.
Этот урок устроен иначе чем предыдущие. Здесь нет длинного списка нового кода. Здесь три части:
- Рефакторинг — небольшие улучшения которые делают проект чище
- Честный разбор — что сделано "учебно" и почему
- Карта дальнейшего пути — что вы уже умеете и куда идти дальше
В Уроке 27 в каждом роутере есть функции _serialize_post, _serialize_comment. Это признак что Pydantic-схемы не полностью берут на себя сериализацию. Правильное решение — использовать model_validate чтобы Pydantic сам собирал объект из ORM-модели.
Для этого добавим в схемы настройку model_config:
# schemas/posts.py — улучшенная версия
from pydantic import BaseModel, Field
from typing import List
from pydantic import ConfigDict
class PostResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
# from_attributes=True — говорит Pydantic читать данные
# из атрибутов объекта, а не только из словаря.
# Это позволяет передавать ORM-объект напрямую в схему.
id: int
title: str
content: str
status: str
created_at: str
author_id: intОднако для полей author_username и tags (которые приходят через relationship) всё равно нужна ручная сборка — ORM-объект не знает что Pydantic ждёт плоский список строк вместо объектов Tag. Поэтому в финальном проекте мы оставляем _serialize_* как есть — это честное учебное решение, а не ошибка.
В продакшне эту задачу решают по-разному: через кастомные
@validator, через специальные поляcomputed_field, или через явное преобразование в репозитории. Единого "правильного" способа нет — есть компромиссы.
Допустимые статусы поста разбросаны по коду. Выносим в одно место:
# constants.py
POST_STATUSES = ['draft', 'published']# schemas/posts.py — добавить валидацию статуса
from constants import POST_STATUSES
from pydantic import field_validator
class PostCreate(BaseModel):
...
status: str = Field(default='published')
@field_validator('status')
@classmethod
def validate_status(cls, v):
if v not in POST_STATUSES:
raise ValueError(f'Статус должен быть одним из: {POST_STATUSES}')
return vТеперь невалидный статус даёт 422 автоматически — без проверок в роутере.
Запустите приложение и пройдите по каждому сценарию из Урока 27:
alembic upgrade head
uvicorn main:app --reloadЧеклист проверки:
□ POST /users — создать пользователя
□ POST /users повторно с тем же email — получить 400
□ POST /tags — создать несколько тегов
□ POST /posts с tag_ids — создать пост
□ GET /posts — получить список
□ GET /posts?tag=python — фильтрация работает
□ GET /posts?author_id=1 — фильтрация по автору
□ GET /posts/1 — полный пост с тегами и автором
□ GET /posts/9999 — получить 404 в правильном формате
□ POST /posts/1/comments — создать комментарий
□ GET /posts/1/comments — получить комментарии
□ DELETE /posts/1 — удалить пост (комментарии каскадно удалятся)
□ GET /health — проверка работоспособности
□ Открыть /docs — убедиться что документация актуальна
Это самая важная часть урока.
Наш Blog API — рабочий прототип. Он делает всё что должен делать: принимает запросы, валидирует данные, работает с базой через ORM, возвращает правильные коды статусов. Если запустить его прямо сейчас — он будет работать.
Но между "рабочим прототипом" и "продакшн-приложением" есть несколько шагов. Ни один из них не перечёркивает то что вы сделали — они просто добавляются сверху на уже правильный фундамент.
1. Аутентификация и авторизация
Как у нас: author_id передаётся в теле запроса — любой может написать пост от имени любого пользователя.
Как в продакшне: пользователь логинится, получает JWT-токен, передаёт его в заголовке Authorization: Bearer <token>. Сервер проверяет токен и извлекает user_id автоматически — клиент его не передаёт.
# Как это выглядит с JWT в FastAPI:
@router.post('/')
def create_post(
data: PostCreate,
current_user: User = Depends(get_current_user) # из токена
):
# current_user.id — уже известен, клиент не передаёт
...Что изучить: python-jose или python-jwt для работы с токенами, passlib для хэширования паролей, OAuth2 в FastAPI.
2. Права доступа
Как у нас: любой может удалить любой пост или комментарий.
Как в продакшне: удалять пост может только его автор (или администратор). Это называется авторизация — проверка прав конкретного действия.
# Проверка прав перед удалением:
if post.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(status_code=403, detail='Нет прав')Что изучить: RBAC (Role-Based Access Control), разрешения в Django REST Framework.
3. Пагинация
Как у нас: GET /posts возвращает все посты — при 10 000 постов это проблема.
Как в продакшне: курсорная или offset-пагинация с метаданными.
{
"items": [...],
"total": 10000,
"page": 1,
"pages": 500,
"has_next": true
}Что изучить: fastapi-pagination, курсорная пагинация для больших объёмов.
4. Валидация email
Как у нас: email: str — принимаем любую строку.
Как в продакшне: проверка формата через EmailStr из pydantic[email].
from pydantic import EmailStr
class UserCreate(BaseModel):
email: EmailStr # автоматически проверяет форматЧто изучить: pip install pydantic[email], дополнительные валидаторы Pydantic.
5. Хранение дат
Как у нас: created_at: str — клиент передаёт дату, мы доверяем формату.
Как в продакшне: дата создания устанавливается сервером автоматически, клиент не может её подделать. Для SQLAlchemy — Column(DateTime, default=func.now()).
# В модели:
from sqlalchemy import DateTime, func
created_at = Column(DateTime, server_default=func.now(), nullable=False)6. Тестирование
Как у нас: ручная проверка через /docs.
Как в продакшне: автоматические тесты которые запускаются при каждом изменении кода.
# Пример теста FastAPI:
from fastapi.testclient import TestClient
def test_create_user():
response = client.post('/users', json={'username': 'test', ...})
assert response.status_code == 201
assert response.json()['username'] == 'test'Что изучить: pytest, httpx, pytest-asyncio, тестирование с базой в памяти sqlite:///:memory:.
7. Переменные окружения
Как у нас: DATABASE_URL = 'sqlite:///blog.db' прямо в коде.
Как в продакшне: секреты (URL базы, секретный ключ JWT) хранятся в переменных окружения или .env файле — не в коде, не в Git.
# settings.py через pydantic-settings:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = 'sqlite:///blog.db'
secret_key: str
class Config:
env_file = '.env'
settings = Settings()8. Логирование и мониторинг
Как у нас: logger.info(...) в нескольких местах.
Как в продакшне: структурированные логи (JSON-формат для агрегаторов), метрики (Prometheus), трейсинг запросов, алерты при ошибках.
Что готово: Что добавить для продакшна:
✓ Архитектура проекта + Аутентификация (JWT)
✓ ORM-модели и миграции + Авторизация (права доступа)
✓ Pydantic-валидация + Пагинация
✓ Dependency Injection + Тесты
✓ Обработка ошибок + Переменные окружения
✓ REST API со всеми методами + Хранение дат на сервере
✓ Связи между сущностями + Email-валидация
✓ Документация /docs + Логирование и мониторинг
Фундамент правильный. Всё перечисленное — это надстройки над уже готовой архитектурой, а не переписывание с нуля.
Посмотрите на этот список не как на пройденные уроки, а как на реальные навыки:
SQL и базы данных:
- Проектировать реляционные схемы с нормализацией
- Писать запросы любой сложности: JOIN, GROUP BY, подзапросы, CTE
- Понимать транзакции и целостность данных
- Работать с SQLite и знать где он ограничен
Python + данные:
- Управлять базой из кода через sqlite3 и SQLAlchemy
- Строить слой данных через паттерн Repository
- Генерировать реалистичные данные через Faker
- Управлять схемой через миграции Alembic
Бэкенд-разработка:
- Понимать HTTP, REST и JSON на уровне протокола
- Строить API на FastAPI с правильной архитектурой
- Валидировать данные через Pydantic
- Изолировать зависимости через DI
- Обрабатывать ошибки единообразно
Архитектура:
- Разделять слои: роутеры, схемы, репозитории, модели
- Понимать зачем каждый слой нужен и что он скрывает
- Читать и писать код который поддерживаем
1. Назовите три вещи которые нужно добавить в Blog API чтобы он был готов к реальному использованию.
Ответ
Любые три из: аутентификация (JWT), авторизация (права доступа), пагинация результатов, автоматические тесты, переменные окружения для секретов, валидация email через EmailStr, хранение дат на сервере, структурированное логирование.
2. Почему author_id передаётся в теле запроса в учебном проекте, и как это решается в продакшне?
Ответ
В учебном проекте нет аутентификации — мы не знаем кто делает запрос. В продакшне пользователь логинится и получает JWT-токен. При каждом запросе токен передаётся в заголовке Authorization. Сервер проверяет токен, извлекает из него user_id и передаёт в эндпоинт через зависимость Depends(get_current_user). Клиент author_id не передаёт — только токен.
3. Что такое курсорная пагинация и чем она лучше offset-пагинации при больших объёмах?
Ответ
Offset-пагинация (LIMIT 20 OFFSET 1000) работает медленнее при большом OFFSET — база данных всё равно читает 1020 строк и выбрасывает первые 1000. Курсорная пагинация использует значение последней записи как "курсор" — WHERE id > 1000 LIMIT 20. База читает ровно 20 строк независимо от глубины страницы. На миллионах записей разница ощутима.
4. Почему DATABASE_URL не должен быть прописан прямо в коде и как это исправить?
Ответ
URL базы данных содержит логин и пароль — это секрет. Если такой файл попадёт в Git (публичный или корпоративный) — секреты станут известны всем. Правильное решение: хранить чувствительные данные в переменных окружения или в .env файле, который добавлен в .gitignore. В коде читаем через os.getenv('DATABASE_URL') или через pydantic-settings.
5. Что означает "фундамент правильный" применительно к нашему Blog API?
Ответ
Архитектурные решения — разделение на слои, паттерн Repository, Dependency Injection, единый формат ошибок, ORM + миграции — сделаны правильно и не нуждаются в переписывании при добавлении продакшн-возможностей. Аутентификация добавляется как новая зависимость, пагинация — как дополнительные query-параметры, тесты — поверх уже правильно изолированных слоёв. Правильный фундамент означает что улучшения добавляются, а не заменяют существующее.
6. Почему понимание SQLAlchemy ORM помогает при изучении Django ORM?
Ответ
Концепции идентичны: модели-классы вместо таблиц, объекты вместо строк, session.query() ≈ Model.objects, .filter() = .filter(), relationship ≈ ForeignKey + related_name, Alembic-миграции ≈ Django-миграции. Django делает то же самое автоматически и с большим количеством встроенных инструментов — но понимание того что происходит под капотом приходит из SQLAlchemy.
7. Что такое from_attributes=True в Pydantic и зачем это нужно при работе с ORM?
Ответ
По умолчанию Pydantic создаёт объекты только из словарей. ORM-объекты — это экземпляры Python-классов с атрибутами, не словари. from_attributes=True (в Pydantic v2) говорит схеме читать данные через атрибуты объекта (obj.name) а не через ключи словаря (data['name']). Это позволяет передавать ORM-объекты напрямую в Pydantic-схемы без ручного преобразования через dict().
8. Какой тип тестов написали бы вы для эндпоинта POST /posts?
Ответ
Минимальный набор: тест успешного создания поста (проверить 201, проверить поля ответа), тест с несуществующим author_id (проверить 404), тест с пустым title (проверить 422), тест с несуществующими tag_ids (проверить корректное поведение), тест что созданный пост появляется в GET /posts. Для изоляции — использовать базу в памяти sqlite:///:memory: и откатывать транзакцию после каждого теста.
9. Почему каскадное удаление реализовано двумя способами одновременно — через ORM и через ondelete='CASCADE' в FK?
Ответ
Каскад ORM (cascade='all, delete-orphan') работает только когда удаление инициируется через SQLAlchemy. ondelete='CASCADE' в базе данных работает всегда — при прямом SQL, при другом инструменте, при удалении через скрипт миграции. Совместное использование — двойная защита целостности данных на разных уровнях. Если убрать один из них — возможны утечки данных в определённых сценариях.
10. Что принципиально нового вы узнаете в курсе по Django, чего нет в этом курсе?
Ответ
Полноценная аутентификация и авторизация из коробки (сессии, токены, группы и разрешения). Административная панель — готовый интерфейс управления данными без написания кода. Встроенная работа с формами и шаблонизатор. Полный стек — от HTML до базы данных в одном фреймворке. Батарейки: email, кэширование, сигналы, management-команды. PostgreSQL как основная СУБД вместо SQLite.