The post has been translated automatically. Original language: Russian
When you start writing a backend on FastAPI, the first endpoints work in an evening. Problems appear later: when there are several developers in the project, the base grows and something needs to be changed. Here are five things that are best laid out from the very beginning.
1. Separate ORM models and Pydantic schemes
The temptation to return an ORM object directly from the endpoint is great, but this way you can accidentally give out extra fields (for example, a password hash). Keep separate schemas for creating, updating, and responding.:
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserRead(BaseModel):
id: int
email: EmailStr
model_config = ConfigDict(from_attributes=True)2. Issue the database session via dependency
This way the connection is guaranteed to close, and it is easy to replace it in tests.:
async def get_session() -> AsyncIterator[AsyncSession]:
async with async_session_maker() as session:
yield session
@router.get("/users/{user_id}", response_model=UserRead)
async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
...3. Migrations via Alembic only
Do not edit the diagram with your hands and do not rely on create_all() outside of the prototype. Migrations to the repository provide a history of database changes and allow the entire team to receive the same schema with one command.
4. Indexes and EXPLAIN ANALYSIS
If you frequently filter or join by a field, it needs an index. Do not guess, but check the query plan.:
EXPLAIN ANALYZE
SELECT * FROM bookings WHERE user_id = 42 AND status = 'active';If you see a Seq Scan on a large table, it's time to think about an index (including a composite one).
5. Think about transactions and configuration in advance
Wrap operations that need to be performed together (for example, to create a reservation and change the status of an object) in a single transaction, otherwise, if the data fails, it will end up in an inconsistent state. And keep the settings (connection string, secrets) in the environment variables via pydantic-settings, not in the code.
Result
None of these points are difficult, but each one saves hours of debugging later. If you put them at the beginning of the project, the code is easier to maintain and expand.
And what kind of rake did you step on in the backend? Share it in the comments, it's interesting to compare the experience.
Когда начинаешь писать бэкенд на FastAPI, первые эндпоинты работают за вечер. Проблемы появляются позже: когда в проекте несколько разработчиков, растёт база и нужно что-то менять. Вот пять вещей, которые лучше заложить с самого начала.
1. Разделяйте ORM-модели и Pydantic-схемы
Соблазн вернуть из эндпоинта ORM-объект напрямую велик, но так можно случайно отдать наружу лишние поля (например, хеш пароля). Держите отдельные схемы для создания, обновления и ответа:
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserRead(BaseModel):
id: int
email: EmailStr
model_config = ConfigDict(from_attributes=True)2. Сессию БД выдавайте через зависимость
Так соединение гарантированно закрывается, а в тестах его легко подменить:
async def get_session() -> AsyncIterator[AsyncSession]:
async with async_session_maker() as session:
yield session
@router.get("/users/{user_id}", response_model=UserRead)
async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
...3. Миграции только через Alembic
Не правьте схему руками и не полагайтесь на create_all() за пределами прототипа. Миграции в репозитории дают историю изменений базы и позволяют всей команде получать одну и ту же схему одной командой.
4. Индексы и EXPLAIN ANALYZE
Если вы часто фильтруете или соединяете по какому-то полю, ему нужен индекс. Не гадайте, а проверяйте план запроса:
EXPLAIN ANALYZE
SELECT * FROM bookings WHERE user_id = 42 AND status = 'active';Если видите Seq Scan на большой таблице, самое время подумать об индексе (в том числе составном).
5. Продумайте транзакции и конфигурацию заранее
Операции, которые должны выполниться вместе (например, создать бронь и изменить статус объекта), оборачивайте в одну транзакцию, иначе при сбое данные окажутся в несогласованном состоянии. А настройки (строку подключения, секреты) храните в переменных окружения через pydantic-settings, а не в коде.
Итог
Ни один из этих пунктов не сложный, но каждый экономит часы отладки позже. Если заложить их в начале проекта, код проще поддерживать и расширять.
А на какие грабли в бэкенде наступали вы? Поделитесь в комментариях, интересно сравнить опыт.