Привет, Хабр! Если вы пишете бэкенд на Python или только собираетесь вкатиться в эту тему, вы наверняка уже слышали про FastAPI. Сейчас его требуют в вакансиях, на нём переписывают легаси-микросервисы, и вообще, кажется, что это главный хайп последних лет в питонячьем мире.
Если после прочтения этой статьи вам захочется системно погрузиться в тему, порешать тесты и закрепить всё на практике, я сделал полноценный и абсолютно бесплатный курс на Stepik —FastAPI для начинающих. Залетайте, буду рад вашей обратной связи!
А сейчас — давайте разбираться с основами прямо здесь.
Что такое FastAPI и почему все о нём говорят?
Если по-научному, то FastAPI — это современный высокопроизводительный веб-фреймворк для создания API на Python 3.8+ (хотя сейчас уже лучше брать Python 3.10+).
Если человеческим языком: это инструмент, который позволяет вам писать меньше кода, получать меньше багов и не тратить время на рутину.
Почему он так взлетел? У него есть три главные «киллер-фичи»:
Скорость работы. FastAPI невероятно быстрый. По тестам производительности он встает в один ряд с NodeJS и Go. Под капотом он стоит на плечах двух гигантов: Starlette (отвечает за асинхронную веб-часть) и Pydantic (молниеносно валидирует данные).
Типизация здорового человека. Помните тайп-хинты (type hints) в Python? Раньше мы писали их чисто для себя и IDE (чтобы PyCharm автокомплитил методы). В FastAPI аннотации типов — это ядро системы. Фреймворк смотрит на типы ваших переменных и сам понимает, как нужно распарсить JSON, как проверить данные и какую ошибку выдать клиенту, если он вместо числа прислал строку.
Автодокументация из коробки. О, это моя любимая часть. Больше не нужно писать YAML-файлы для Swagger вручную! Вы просто пишете код Python, запускаете сервер, заходите на
/docs— и видите красивую, интерактивную документацию Swagger UI. Она генерируется сама. Для новичков (да и для фронтендеров, которые будут работать с вашим API) это всегда чистый вау-эффект.
Django vs Flask vs FastAPI: где мы находимся?
Чтобы вам было проще ориентироваться, давайте сделаем короткое сравнение:
Django: Это огромный швейцарский нож. В нём есть всё: ORM, админка, шаблоны. Идеален для монолитов. Но если вам нужно написать просто три эндпоинта для микросервиса — брать Django это как стрелять из базуки по воробьям. Слишком тяжело и много лишнего.
Flask: Максимально минималистичный фреймворк. «Голый» Flask прекрасен, но как только вам нужно написать серьезный API с валидацией входных данных и документацией, вам придется установить штук пять сторонних библиотек (вроде
marshmallowилиflask-restful) и заставить их работать вместе.FastAPI: Золотая середина для API. Он такой же легкий на старте, как Flask, но сразу имеет встроенные, стандартизированные инструменты для валидации и документации. Он создан специально для того, чтобы делать API и делать это круто.
Что мы напишем?
Talk is cheap, show me the code. Лучший способ выучить инструмент — сделать на нём что-то полезное.
Мы с вами напишем классику — API для управления списком задач (To-Do List).
Чтобы не перегружать статью и не отвлекаться на настройку баз данных (оставим SQLAlchemy, PostgreSQL и миграции для следующего раза), мы будем хранить наши задачки просто в оперативной памяти (в обычном Python-списке).
Наша задача — понять саму философию фреймворка: как он принимает запросы, как проверяет данные, как отдает ответы и как правильно структурировать проект, чтобы код не превратился в лапшу.
Шаг 1. Установка и первый запуск (Быстрый старт)
Переходим к практике. Я всегда советую начинать с чистого виртуального окружения (virtualenv), чтобы не засорять глобальный Python. Создали, активировали? Отлично. Теперь ставим сам фреймворк.
Выполняем в терминале:
pip install "fastapi[all]"
Небольшая ремарка от сеньора: почему мы пишем [all]? Дело в том, что сам по себе пакет fastapi — это просто каркас. Он не умеет самостоятельно принимать HTTP-запросы, для этого ему нужен веб-сервер. Флаг [all] говорит pip'у: «Установи мне FastAPI и сразу подтяни всё необходимое для комфортной работы». Вместе с фреймворком установится Uvicorn (молниеносный ASGI-сервер, который будет крутить наше приложение), библиотеки для валидации email-адресов и прочие полезные утилиты.
Теперь создаем в папке проекта файл main.py и пишем те самые 5 строчек кода, с которых начинается любой путь:
from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"message": "Привет, Хабр!"}
Давайте быстро разберем, что здесь происходит:
Мы импортируем класс
FastAPI.Создаем экземпляр приложения
app = FastAPI(). Именно этот объект будет ядром нашей программы.Вешаем декоратор
@app.get("/"). Он говорит фреймворку: «Если придет HTTP-запрос методом GET по корневому адресу (то есть/), вызови функцию под этим декоратором».Пишем саму функцию. Обратите внимание, мы возвращаем обычный Python-словарь (dict). FastAPI под капотом сам превратит его в правильный JSON-ответ.
Запуск сервера
Код написан, пора его оживить. В терминале пишем:
uvicorn main:app --reload
Для новичков эта команда часто выглядит как заклинание, поэтому давайте её расшифруем:
uvicorn— наш сервер.main— имя файла (main.py), в котором лежит наш код.app— имя переменной внутриmain.py, в которую мы положили экземплярFastAPI().--reload— флаг горячей перезагрузки. Это мастхэв для локальной разработки: теперь, если вы измените код вmain.pyи сохраните файл, сервер сам моментально перезапустится. В продакшене, конечно, этот флаг использовать не нужно.
Если всё сделано правильно, в консоли появится зеленая надпись: Application startup complete.
Магия FastAPI: где моя документация?
Откройте браузер и перейдите по адресу http://127.0.0.1:8000. Вы увидите белый экран и надпись {"message":"Привет, Хабр!"}. Работает! Но этим сейчас никого не удивишь.
А теперь добавьте к урлу /docs, чтобы получилось http://127.0.0.1:8000/docs, и нажмите Enter.
Добро пожаловать в магию FastAPI. Перед вами полноценный, красивый Swagger UI. Фреймворк проанализировал ваш код и сам сгенерировал интерактивную документацию.
Здесь вы можете не просто смотреть, какие у вашего API есть эндпоинты, но и тестировать их! Раскройте плашку GET-запроса, нажмите кнопку «Try it out» (Попробовать), затем «Execute» (Выполнить). Ваш браузер прямо отсюда отправит запрос к серверу и покажет ответ.
Postman и cURL пока могут отдохнуть — для разработки и дебага базовых вещей встроенного Swagger'а хватает за глаза.
Шаг 2. Маршрутизация (Роутинг): как API понимает, чего мы хотим
Окей, отдавать захардкоженный «Привет, Хабр!» — это весело, но реальные API работают с динамическими данными. Клиент должен уметь попросить у нас конкретную задачу или, скажем, список из 10 последних задач.
В вебе для этого используются два основных механизма передачи параметров прямо в URL: параметры пути (Path parameters) и параметры запроса (Query parameters). Давайте посмотрим, как изящно FastAPI с ними работает.
Параметры пути (Path Parameters)
Представьте, что мы хотим получить информацию о конкретной задаче по её ID. В URL это будет выглядеть так: http://127.0.0.1:8000/tasks/1.
Добавим новый эндпоинт в наш main.py:
@app.get("/tasks/{task_id}") def get_task(task_id: int): return {"task_id": task_id, "name": f"Задача номер {task_id}"}
Смотрите, что мы сделали:
В декораторе мы указали
{task_id}. Это плейсхолдер. FastAPI понимает, что всё, что будет написано после/tasks/, нужно положить в переменнуюtask_id.В аргументах функции мы написали
task_id: int.
И вот тут начинается магия типизации FastAPI.
Раньше (например, во Flask) вам пришлось бы внутри функции писать int(task_id), оборачивать это в try/except, чтобы поймать ошибку ValueError, если какой-то умник вместо числа передаст буквы.
В FastAPI вы просто ставите аннотацию типа : int. Фреймворк берет эту работу на себя. Если вы перейдете по адресу /tasks/5, вы получите:
{"task_id": 5, "name": "Задача номер 5"} (обратите внимание, 5 — это число в JSON, а не строка).
Но что будет, если клиент ошибется и запросит /tasks/hello?
Сервер не упадет с Internal Server Error (500). FastAPI перехватит запрос на подлете, поймет, что "hello" нельзя превратить в int, и вежливо вернет клиенту статус 422 Unprocessable Entity с очень подробным объяснением ошибки:
{ "detail":[ { "type": "int_parsing", "loc": ["path", "task_id"], "msg": "Input should be a valid integer, unable to parse string as an integer", "input": "hello" } ] }
Фронтендеры скажут вам за такие ответы огромное спасибо! Им сразу понятно: ошибка в пути (path), в поле task_id, ожидалось число, а прислали "hello".
Параметры запроса (Query Parameters)
Теперь другая ситуация. Мы хотим получить список задач, но в базе их миллион. Отдавать все сразу нельзя — сервер «ляжет», а браузер клиента зависнет. Нужна пагинация (постраничный вывод).
Для этого используют параметры запроса — это то, что идет в URL после вопросительного знака: /tasks?skip=0&limit=10.
Как FastAPI понимает, что есть Path, а что Query? Очень просто: если аргумент функции не объявлен в пути (внутри {}), FastAPI автоматически считает его параметром запроса.
Добавим такой код:
@app.get("/tasks") def get_tasks(skip: int = 0, limit: int = 10): return { "message": "Возвращаем список задач", "skip": skip, "limit": limit }
Здесь мы задали значения по умолчанию: skip: int = 0 и limit: int = 10.
Что это дает на практике?
Если клиент запросит просто
/tasks(вообще без параметров), FastAPI подставит дефолтные значения. Ответ будет:{"message": "...", "skip": 0, "limit": 10}.Если клиент захочет вторую страницу и запросит
/tasks?skip=10&limit=10, FastAPI сам найдет эти параметры в URL, сам превратит их в числа (int) и передаст в функцию.
И да, валидация здесь работает точно так же! Запросите /tasks?limit=много — и получите красивую 422 ошибку.
Лайфхак: прямо сейчас откройте вкладку со Swagger UI (
/docs) и обновите страницу. Вы увидите, что там появились наши новые эндпоинты/tasksи/tasks/{task_id}. Если вы раскроете их, Swagger покажет, какие параметры обязательные (с красной звездочкой), а какие опциональные (со значениями по умолчанию), и позволит поиграться с ними через кнопочки.
Шаг 3. Pydantic — сердце FastAPI (Работа с телом запроса)
Итак, мы научились передавать простые параметры прямо в URL. Но что, если нам нужно создать новую задачу? Передавать длинный текст, описание и статус выполнения через адресную строку — плохая идея. Для сложных и объемных данных в REST API используется тело запроса (Request Body), и обычно оно передается в формате JSON через метод POST.
И вот здесь на сцену выходит Pydantic — библиотека, которая является настоящим сердцем FastAPI.
Прежде чем писать код, давайте на секунду остановимся и разберем два важных термина, которые вы будете слышать постоянно: валидация и сериализация.
С точки зрения сеньора, если вы понимаете эти два слова, вы уже наполовину поняли, как работает любой современный бэкенд.
Валидация — это фейсконтроль. Представьте строгого охранника на входе в клуб. Клиент (фронтенд) присылает нам JSON с данными. Задача фейсконтроля — проверить: «А есть ли у тебя обязательное поле
title? А полеis_completed— это точно логическое значение (True/False), а не строка "картошка"?». Если данные кривые, охранник разворачивает клиента с ошибкой.Сериализация (и де��ериализация) — это переводчик. Наш Python-код не умеет напрямую работать с сырым JSON-текстом, ему нужны удобные питонячьи классы и объекты. А клиент в браузере не понимает объекты Python, ему нужен JSON. Pydantic работает как идеальный переводчик: на входе превращает JSON в объекты Python, а на выходе — объекты Python обратно в JSON.
Создаем первую Pydantic-модель
Давайте опишем, как должна выглядеть наша задача (Task). Для этого мы создадим класс, который наследуется от BaseModel.
Добавим в наш main.py импорт Pydantic и опишем модель:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() # Наша Pydantic-модель class Task(BaseModel): title: str description: str | None = None # Необязательное поле, по умолчанию None is_completed: bool = False # По умолчанию задача не выполнена
Что мы здесь написали?
У задачи обязательно должен быть заголовок (
title) в виде строки (str).Описание (
description) — это тоже строка, но оно необязательное (спасибо| Noneи значению по умолчаниюNone).Статус (
is_completed) — булево значение, которое по умолчаниюFalse(ведь логично, что новая задача создается невыполненной).
Пишем POST-запрос на создание задачи
Теперь давайте сделаем эндпоинт, который будет принимать эти данные.
@app.post("/tasks") def create_task(task: Task): # Здесь task — это уже готовый объект Pydantic, а не просто сырой словарь! # Мы можем обращаться к полям через точку: task.title, task.description # Имитируем сохранение в базу данных и возвращаем ответ return { "message": "Задача успешно создана", "task": task }
Смотрите, как изящно: мы просто указали task: Task в аргументах функции.
В старые добрые времена во Flask или Django (без DRF) нам пришлось бы писать кучу проверок:
«А прислали ли нам JSON? А есть ли там ключ title? А если ключа is_completed нет, надо подставить False…». Код разрастался бы на десятки строк.
Здесь всё делает FastAPI под капотом.
Как работает магия проверки (и почему мы любим 422 ошибку)
Запустите сервер (или просто откройте Swagger UI на http://127.0.0.1:8000/docs, если он у вас уже крутится с флагом --reload). Найдите новый зеленый блок POST /tasks.
Нажмите «Try it out» и отправьте правильный JSON:
{ "title": "Купить молоко", "description": "И заодно хлеб" }
Вы получите статус 200 OK и вашу сформированную задачу (где is_completed автоматически стал false).
А теперь давайте побудем плохими парнями и отправим кривые данные. Уберем обязательное поле title и попытаемся сломать тип данных:
{ "description": "Сломать сервер", "is_completed": "какая-то строка вместо True/False" }
Что сделает FastAPI? Сервер не упадет. В консоли не посыплются страшные трейсбеки. Наш строгий охранник Pydantic просто не пустит этот запрос внутрь функции create_task.
Клиент моментально получит всё тот же красивый статус 422 Unprocessable Entity (Необрабатываемая сущность) с четким списком претензий:
{ "detail":[ { "type": "missing", "loc": ["body", "title"], "msg": "Field required", "input": {"description": "Сломать сервер", "is_completed": "какая-то строка вместо True/False"} }, { "type": "bool_parsing", "loc": ["body", "is_completed"], "msg": "Input should be a valid boolean, unable to interpret input", "input": "какая-то строка вместо True/False" } ] }
Перевод на человеческий:
В теле запроса (
body) отсутствует обязательное полеtitle.В теле запроса (
body) полеis_completedне удалось превратить вboolean.
И на это вы не потратили ни единой строчки кода! Вы просто объявили класс. Это именно то, за что разработчики так сильно любят FastAPI.
Шаг 4. Практика: пишем CRUD для нашего To-Do List
Хватит теории, давайте напишем что-то, что можно пощупать. Любой, даже самый сложный энтерпрайз-проект, в основе своей сводится к одной аббревиатуре — CRUD.
Create — создание (POST)
Read — чтение (GET)
Update — обновление (PUT/PATCH)
Delete — удаление (DELETE)
Чтобы не раздувать статью и не мучить вас настройкой PostgreSQL, SQLAlchemy и миграций (об этом поговорим в другой раз), мы будем использовать базу данных в памяти. То есть, обычный питоновский список (list), в который мы будем складывать наши задачки. Да, при перезапуске сервера данные пропадут, но для понимания логики API это идеальный вариант.
Давайте немного обновим наш код. Мы добавим в Pydantic-модель поле id (чтобы к задаче можно было обращаться) и напишем все 4 эндпоинта.
Вот как выглядит наш готовый main.py на данном этапе:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() # Наша импровизированная база данных fake_database =[] # Pydantic-модель для валидации входящих данных class Task(BaseModel): title: str description: str | None = None is_completed: bool = False # 1. READ: Получить все задачи @app.get("/tasks") def get_tasks(): return fake_database # 2. CREATE: Создать новую задачу @app.post("/tasks") def create_task(task: Task): # Генерируем ID для новой задачи (просто длина списка + 1) new_task = task.dict() # Превращаем Pydantic-модель в словарь new_task["id"] = len(fake_database) + 1 # Сохраняем в "базу" fake_database.append(new_task) return new_task # 3. UPDATE: Обновить задачу по ID @app.put("/tasks/{task_id}") def update_task(task_id: int, task: Task): # Ищем задачу по ID for idx, t in enumerate(fake_database): if t["id"] == task_id: # Обновляем данные, сохраняя оригинальный ID updated_task = task.dict() updated_task["id"] = task_id fake_database[idx] = updated_task return updated_task # Если цикл завершился и мы ничего не вернули — задачи нет raise HTTPException(status_code=404, detail="Задача не найдена") # 4. DELETE: Удалить задачу по ID @app.delete("/tasks/{task_id}") def delete_task(task_id: int): for idx, t in enumerate(fake_database): if t["id"] == task_id: del fake_database[idx] return {"message": "Задача успешно удалена"} # Если задача не найдена, выбрасываем ошибку raise HTTPException(status_code=404, detail="Задача не найдена")
Что здесь происходит? Разбор полетов от сеньора
Создание и Чтение (
GETиPOST)
С ними мы уже знакомы. Обратите внимание на методtask.dict()(в новых версиях Pydantic можно использоватьtask.model_dump()). Поскольку клиент присылает нам JSON без ID (ведь задачу мы только создаем), мы берем готовую отвалидированную модель, превращаем её в словарь, добавляем тудаidи кладем в наш списокfake_database.Обновление и Удаление (
PUTиDELETE)
Здесь мы используем параметры путиtask_id. Клиент говорит: «Обнови мне задачу номер 5». Мы перебираем нашу базу данных, и если находим задачу сid == 5, то заменяем её или удаляем.
Как изящно возвращать ошибки (HTTPException)
А вот тут самое интересное. Что делать, если клиент хочет удалить задачу №999, которой не существует?
В плохом API сервер упадет с ошибкой индекса или вернет пустую страницу. В нормальном API мы должны вернуть HTTP-статус 404 Not Found.
В FastAPI для этого есть специальный инструмент — класс HTTPException. Вы просто делаете raise HTTPException(...) в любом месте вашего кода.
Как только FastAPI видит этот raise, он немедленно прерывает выполнение функции и отправляет клиенту красивый JSON-ответ с нужным статус-кодом:
{ "detail": "Задача не найдена" }
Это безумно удобно! Вам не нужно городить сложные конструкции из return, передавать объекты ответов (как объект Response во Flask). Вы просто «выбрасываете» исключение с нужным кодом (400, 401, 403, 404), а фреймворк сам пакует его в правильный HTTP-ответ.
Попробуйте сами:
Зайдите в/docs. Создайте пару задач черезPOST /tasks(посмотрите, как им автоматически присваиваются ID: 1, 2...). Затем вызовитеGET /tasksи убедитесь, что они сохранились. После этого попробуйте удалить задачу с ID 1, а затем снова запросите список всех задач. Ваш первый полноценный API работает!
Шаг 5. Dependency Injection (Внедрение зависимостей) — просто о сложном
Если вы пришли в Python недавно, словосочетание Dependency Injection (внедрение зависимостей) может вызвать у вас нервную дрожь и ассоциации со страшным кровавым энтерпрайзом на Java. Спокойно! Выдыхаем. В FastAPI эта штука реализована настолько элегантно и просто, что вы будете использовать её каждый день и радоваться.
Давайте я объясню на пальцах, как сеньор джуну.
Что такое зависимость в FastAPI? Это просто функция, которая выполняется ДО вашего эндпоинта, чтобы подготовить для него данные или проверить какие-то условия.
Представьте, что вы — большой босс (ваш эндпоинт). К вам в кабинет должен зайти посетитель (HTTP-запрос). Но перед тем как посетитель попадет к вам, он проходит через вашего секретаря (зависимость). Секретарь проверяет у него паспорт, берет нужные документы, аккуратно складывает их в папочку и кладет вам на стол. Вы не тратите время на рутину — вы сразу берете готовую папочку и делаете свою работу.
Зачем нам это нужно? (Проблема)
Вспомните наш Шаг 2, где мы делали пагинацию с помощью параметров запроса: skip и limit.
Обычно в реальном API у вас не один эндпоинт, а десятки. Вы можете запрашивать список задач, список пользователей, список комментариев. И везде нужна пагинация.
Писать в каждой функции (skip: int = 0, limit: int = 10) — это нарушение главного правила программиста: DRY (Don't Repeat Yourself — не повторяйся). А если завтра мы захотим, чтобы дефолтный limit был не 10, а 20? Придется менять код в 50 местах.
Решение через Depends()
Давайте вынесем логику пагинации в отдельную функцию-секретаря (нашу зависимость) и внедрим её в эндпоинт с помощью специального инструмента Depends.
Обновляем наш код:
from fastapi import FastAPI, Depends app = FastAPI() fake_database =[{"id": 1, "title": "Задача 1"}, {"id": 2, "title": "Задача 2"}] # 1. Пишем функцию-зависимость (нашего "секретаря") def pagination_parameters(skip: int = 0, limit: int = 10): return {"skip": skip, "limit": limit} # 2. Внедряем её в эндпоинт с помощью Depends @app.get("/tasks") def get_tasks(pagination: dict = Depends(pagination_parameters)): # Вытаскиваем подготовленные данные skip = pagination["skip"] limit = pagination["limit"] # Возвращаем кусок базы данных согласно пагинации return fake_database[skip : skip + limit]
Как работает эта магия?
Когда клиент делает запрос GET /tasks?skip=0&limit=5, FastAPI видит в аргументах Depends(pagination_parameters).
Что делает фреймворк:
Он ставит выполнение функции
get_tasksна паузу.Идет в функцию
pagination_parameters.Читает из URL параметры
skipиlimit, валидирует их (да, Pydantic здесь тоже работает!).Выполняет
pagination_parametersи получает результат (словарь{"skip": 0, "limit": 5}).Возвращается в
get_tasks, кладет этот словарь в переменнуюpaginationи запускает ваш код.
Бинго! Ваш эндпоинт чист и лаконичен. Он занимается только бизнес-логикой (отдает задачи), а всю грязную работу по парсингу параметров взял на себя Depends.
Откровение от сеньора:
Пагинация — это лишь простейший пример. В реальных проектах на FastAPI черезDepends()делается вообще всё.
Нужен доступ к базе данных?
db: Session = Depends(get_db).Нужно проверить, авторизован ли юзер?
user = Depends(get_current_user). Внутриget_current_userFastAPI сам достанет токен из заголовков, расшифрует его, сходит в БД, проверит права и вернет вам готовый объект пользователя (или выкинет 401 ошибку, даже не пустив юзера в эндпоинт).
И самое крутое: в Swagger UI (/docs) эта пагинация отобразится точно так же, как если бы вы написали параметры напрямую в эндпоинте. Фреймворк проанализирует зависимость и добавит нужные поля в документацию.
Шаг 6. Асинхронность (async / await) в FastAPI: когда писать, а когда нет
Если вы обратили внимание на код в предыдущих шагах, то могли заметить странную вещь. Вроде бы FastAPI славится тем, что он современный и асинхронный, но мы везде писали обычные синхронные функции: def get_tasks(). И всё работало!
Почему так? Чтобы это понять, давайте проведем короткий ликбез по асинхронности в Python, и я расскажу вам главное правило FastAPI, незнание которого — это самая частая причина «падения» серверов у новичков.
Короткий ликбез: def против async def
Представьте, что наш сервер — это повар в ресторане фастфуда (Event Loop).
Синхронный подход (обычный
def): Повар принимает заказ на бургер, кладет котлету на гриль и... стоит смотрит на нее 5 минут, пока она жарится. Очередь из клиентов растет, все злятся, но повар ни на кого не обращает внимания, пока не дожарит. В мире кода это называется блокирующая операция (например, долгий запрос к базе данных или скачивание файла по сети).Асинхронный подход (
async defиawait): Повар кладет котлету на гриль, заводит таймер (это нашawait) и тут же поворачивается к следующему клиенту в очереди, чтобы принять новый заказ. Когда таймер звенит, повар на секунду отвлекается, чтобы снять котлету. Никто не ждет без дела, производительность максимальная.
Если ваш код делает запросы к другим API, читает файлы или ходит в базу данных, асинхронность позволит вашему серверу обрабатывать тысячи запросов в секунду вместо десятков.
Золотое правило FastAPI
Итак, как же правильно писать эндпоинты в FastAPI? Запомните (а лучше распечатайте и повесьте над монитором) это простое правило из двух пунктов:
1. Используйте async def, если внутри функции вы используете await.
Если вы подключили асинхронную базу данных (например, asyncpg или асинхронную алхимию) или делаете асинхронные HTTP-запросы через библиотеку httpx, ваш код должен выглядеть так:
import httpx from fastapi import FastAPI app = FastAPI() @app.get("/pokemon") async def get_pokemon(): # Мы используем асинхронный клиент и ключевое слово await # Пока мы ждем ответ от pokeapi, FastAPI пойдет обслуживать других клиентов! async with httpx.AsyncClient() as client: response = await client.get("https://pokeapi.co/api/v2/pokemon/ditto") return response.json()
2. Используйте обычный def, если ваша функция работает синхронно или делает тяжелые вычисления.
Если вы используете старую добрую библиотеку requests, синхронную базу данных (обычный psycopg2) или ваша функция просто долго считает числа Фибоначчи, никогда не пишите async def. Пишите просто def:
import time from fastapi import FastAPI app = FastAPI() @app.get("/heavy-computation") def do_heavy_stuff(): # Имитация долгой синхронной работы (например, тяжелый SQL-запрос) time.sleep(5) return {"message": "Ух, это было тяжело!"}
Магия под капотом: почему FastAPI такой умный?
А теперь следите за руками, сейчас будет магия фреймворка.
Если вы напишете time.sleep(5) внутри async def, ваш повар из примера выше упадет в обморок на 5 секунд. Весь сервер зависнет. Ни один другой клиент не получит ответ, пока эти 5 секунд не пройдут. Это классическая ошибка джуна.
Но если вы напишете тот же time.sleep(5) внутри обычного def (как в коде выше), FastAPI не зависнет!
Почему? Потому что FastAPI достаточно умен. Когда он видит обычный def, он думает: "Ага, этот парень может заблокировать мне работу. Отправлю-ка я его выполняться в отдельном потоке (Thread Pool)". В терминах нашего ресторана: FastAPI мгновенно нанимает временного повара-помощника, отдает ему эту задачу и продолжает принимать заказы в основном потоке.
Итоговый вывод для новичка:
Не знаете, поддерживает ли ваша база данных асинхронность? Пишите
def.Делаете сложные математические расчеты, ресайзите картинки, парсите огромные Excel-файлы? Пишите
def.Точно уверены, что библиотека асинхронная, и знаете, куда поставить
await? Пишитеasync def.
FastAPI всё сделает правильно за вас в обоих случаях, главное — не пытайтесь его обмануть, вставляя синхронные «тормоза» внутрь асинхронных функций!
Шаг 7. Правильная архитектура (Уходим от одного файла main.py)
Если вы дошли до этого шага и всё повторили в коде — поздравляю, у вас есть полностью рабочий API! Но есть одна проблема. Сейчас весь наш код (модели, фейковая база данных, эндпоинты) лежит в одном файле main.py.
Для учебного проекта из 50 строк это нормально. Но в реальной жизни ваш To-Do List обрастет пользователями, категориями, тегами, комментариями. Если вы продолжите писать всё в main.py, через пару месяцев этот файл превратится в монстра на 5000 строк. Искать там баги будет больнее, чем наступать на детали Лего в темноте. В программировании это называется антипаттерном God Object (Божественный объект).
Сеньоры так не делают. Поэтому финальный аккорд нашего погружения — правильная структуризация проекта.
Как разбить проект?
Давайте создадим базовую, но легко масштабируемую структуру папок и файлов:
my_project/ ├── main.py # Точка входа в приложение (здесь живет app) ├── models.py # Наши Pydantic-схемы (описание данных) └── routers/ # Папка с роутерами (нашими эндпоинтами) └── tasks.py # Вся логика, связанная только с задачами
(Небольшая ремарка: в больших проектах часто делают разделение: models.py используют для моделей базы данных SQLAlchemy, а schemas.py — для Pydantic-схем. Но чтобы вас не путать, пока оставим всё в models.py, как договаривались).
Давайте перенесем наш код по этим файлам.
1. Файл models.py
Сюда мы уносим только описание того, как выглядят наши данные. Никакой логики API тут нет.
# models.py from pydantic import BaseModel class Task(BaseModel): title: str description: str | None = None is_completed: bool = False
2. Файл routers/tasks.py и магия APIRouter
Здесь будет жить наш CRUD. Но мы больше не можем использовать @app.get(), потому что переменная app останется в main.py.
На помощь приходит APIRouter — это, грубо говоря, "мини-FastAPI". Вы можете создавать такие мини-приложения для разных сущностей (задачи, юзеры, платежи), а потом собирать их как конструктор в главном файле.
# routers/tasks.py from fastapi import APIRouter, HTTPException from models import Task # Импортируем нашу модель # Создаем роутер. # Обратите внимание на prefix и tags — это супер-удобно! router = APIRouter( prefix="/tasks", tags=["Задачи"] ) fake_database =[] # Теперь вместо @app.get("/tasks") мы пишем @router.get("") # Путь "/tasks" подставится автоматически из prefix! @router.get("") def get_tasks(): return fake_database @router.post("") def create_task(task: Task): new_task = task.model_dump() # или task.dict() для старых версий new_task["id"] = len(fake_database) + 1 fake_database.append(new_task) return new_task @router.delete("/{task_id}") def delete_task(task_id: int): for idx, t in enumerate(fake_database): if t["id"] == task_id: del fake_database[idx] return {"message": "Задача успешно удалена"} raise HTTPException(status_code=404, detail="Задача не найдена")
Смотрите, как круто: благодаря prefix="/tasks" при создании роутера, нам больше не нужно писать слово "tasks" в каждом эндпоинте. Код стал чище! А tags=["Задачи"] красиво сгруппирует эти эндпоинты в нашем Swagger UI.
3. Файл main.py (Точка входа)
Теперь посмотрим на наш главный файл. Он похудел, похорошел и стал отвечать только за одну вещь — запуск и сборку приложения.
# main.py from fastapi import FastAPI from routers import tasks # Импортируем наш роутер задач app = FastAPI( title="Мой супер To-Do API", description="API для управления задачами, написанное по статье с Хабра", version="1.0.0" ) # Подключаем роутер к главному приложению app.include_router(tasks.router) @app.get("/") def root(): return {"message": "Добро пожаловать в API! Перейдите на /docs для документации."}
Всё! Метод app.include_router() берет все маршруты, которые мы описали в routers/tasks.py, и «приклеивает» их к нашему главному приложению.
Если завтра вам понадобится добавить регистрацию пользователей, вы просто создадите файл routers/users.py, напишете там логику, и добавите ровно одну строчку в main.py: app.include_router(users.router). Архитектура позволяет проекту расти бесконечно, не превращаясь в помойку.
Запустите сервер:
uvicorn main:app --reload. Перейдите наhttp://127.0.0.1:8000/docs.
Вы увидите, что документация стала еще красивее: появилось кастомное название API, описание, а задачи аккуратно собрались в группу (тэг) «Задачи».
Заключение
Вот мы и подошли к концу нашего стремительного погружения. Давайте кратко подытожим, что мы успели сделать всего за одну статью:
Подняли сервер с нуля и познакомились с магией автодокументации (Swagger UI).
Научились передавать данные через параметры пути и запроса (роутинг).
Поняли, почему Pydantic — это строгий, но справедливый охранник для нашего тела запроса (валидация и сериализация).
Написали полноценный CRUD-интерфейс с правильными HTTP-статусами ошибок (404 и 422).
Пощупали Dependency Injection (
Depends()) и поняли, что это удобно.Разобрались с главным правилом асинхронности (когда писать
def, а когдаasync def).Разнесли код по разным файлам с помощью
APIRouter, заложив фундамент для правильной архитектуры.
Согласитесь, для старта звучит внушительно! Но, конечно, наш To-Do List в оперативной памяти — это пока лишь игрушка.
Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.
Спасибо всем, кто дочитал! Надеюсь, мне удалось показать вам, почему FastAPI так стремительно завоевал любовь Python-сообщества.
