Разработка веб приложений на Python с помощью FastAPI: от простого до сложного

Раздел: Веб-разработка -> Разработка веб-приложений

Основной способ: минимальное FastAPI приложение

FastAPI является современным вебфреймворком для Python, ориентированным на создание высокопроизводительных API. Для начала необходимо установить библиотеки: pip install fastapi uvicorn. После установки создается файл main.py с приложением.


from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

Fastapi python приложение (создание приложения на fastapi и python)

Для запуска используется команда uvicorn main:app --reload. Флаг --reload автоматически перезагружает сервер при изменениях кода. Приложение становится доступным по адресу http://127.0.0.1:8000. Документация Swagger доступна по /docs, ReDoc по /redoc. Это базовое приложение служит фундаментом для дальнейших расширений.

Типичные ошибки и их решения

  • Ошибка импорта: если библиотеки не установлены, появляется ModuleNotFoundError. Решение – выполнить pip install fastapi uvicorn.
  • Порт занят: ошибка Address already in use. Рекомендуется указать другой порт, например uvicorn main:app --port 8001.
  • Не работает reload: следует убедиться, что установлен watchfiles (установка pip install watchfiles).

Как добавить валидацию входных данных с помощью Pydantic?

Pydantic интегрирован в FastAPI по умолчанию. Для валидации тела запроса и параметров пути/строки используются модели Pydantic. Пример:


from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = False

@app.post("/items/")
async def create_item(item: Item):
    return {"item_id": 1, **item.dict()}

FastAPI автоматически проверяет типы данных, возвращает ошибку 422 Unprocessable Entity при несоответствии.

Проблема: поле не обязательное, но не передается по умолчанию.

Если поле is_offer не указано, оно примет значение False. Если нужно сделать поле обязательным, не следует задавать значение по умолчанию.

Как организовать зависимости для повторного использования?

Зависимости (Dependencies) позволяют вынести общую логику, например проверку токена или соединение с БД. Используется Depends.


from fastapi import Depends, FastAPI

def common_parameter(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameter)):
    return commons

Функция common_parameter принимает параметры запроса и возвращает словарь. Depends автоматически внедряет возвращаемое значение.

Как обрабатывать фоновые задачи?

Используется BackgroundTasks для выполнения операций после отправки ответа. Например, отправка email.


from fastapi import BackgroundTasks, FastAPI

def write_log(message: str):
    with open("log.txt", "a") as f:
        f.write(message + '\n')

@app.post("/send-notification/")
async def send_notification(background_tasks: BackgroundTasks, email: str):
    background_tasks.add_task(write_log, f"Notification sent to {email}")
    return {"message": "Notification sent"}

Ошибка: фоновые задачи не выполняются при использовании Gunicorn с worker'ами.

BackgroundTasks работают только в рамках одного процесса. Для распределенных систем рекомендуется использовать очередь задач (Celery, RQ).

Как настроить CORS для взаимодействия с фронтендом?

CORS (Cross-Origin Resource Sharing) разрешает запросы с других доменов. FastAPI имеет встроенную поддержку через CORSMiddleware.


from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

В allow_origins перечисляются разрешенные источники. Для разработки можно указать ["*"], но для продакшна рекомендуется указывать конкретные домены.

Как реализовать работу с базой данных через SQLAlchemy?

FastAPI не навязывает ORM, но SQLAlchemy является популярным выбором. Пример асинхронного использования с PostgreSQL:


from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/dbname"
engine = create_async_engine(DATABASE_URL)
SessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

async def get_db():
    async with SessionLocal() as session:
        yield session

Затем в маршруте используется Depends(get_db) для получения сессии.

Проблема: несоответствие синхронных и асинхронных драйверов.

Для асинхронного доступа к БД следует использовать драйверы asyncpg (для PostgreSQL) или aiomysql. Синхронные драйверы (psycopg2) блокируют event loop.

Как добавить обработку ошибок и собственные исключения?

FastAPI позволяет определить пользовательские исключения и обработчики. Пример:


from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse

class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name

@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something."},
    )

@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name)
    return {"unicorn_name": name}

Также можно использовать встроенное HTTPException для стандартных кодов ошибок.

Как написать тесты для FastAPI приложения?

FastAPI предоставляет TestClient на основе starlette. Пример теста:


from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

Запуск тестов выполняется командой pytest. Важно установить библиотеку httpx для TestClient.

Примеры расширенных сценариев

Полноценное CRUD приложение с SQLAlchemy

Реализуем простой блог – сущность Post с полями id, title, content. Используем синхронный SQLite для демонстрации.

Пример

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel

app = FastAPI()

SQLALCHEMY_DATABASE_URL = "sqlite:///./blog.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class PostDB(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    content = Column(String)

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

class PostCreate(BaseModel):
    title: str
    content: str

class PostResponse(BaseModel):
    id: int
    title: str
    content: str

    class Config:
        orm_mode = True

@app.post("/posts/", response_model=PostResponse)
def create_post(post: PostCreate, db: Session = Depends(get_db)):
    db_post = PostDB(**post.dict())
    db.add(db_post)
    db.commit()
    db.refresh(db_post)
    return db_post

@app.get("/posts/", response_model=list[PostResponse])
def read_posts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
    posts = db.query(PostDB).offset(skip).limit(limit).all()
    return posts

@app.get("/posts/{post_id}", response_model=PostResponse)
def read_post(post_id: int, db: Session = Depends(get_db)):
    post = db.query(PostDB).filter(PostDB.id == post_id).first()
    if post is None:
        raise HTTPException(status_code=404, detail="Post not found")
    return post

@app.put("/posts/{post_id}", response_model=PostResponse)
def update_post(post_id: int, post: PostCreate, db: Session = Depends(get_db)):
    db_post = db.query(PostDB).filter(PostDB.id == post_id).first()
    if db_post is None:
        raise HTTPException(status_code=404, detail="Post not found")
    for key, value in post.dict().items():
        setattr(db_post, key, value)
    db.commit()
    db.refresh(db_post)
    return db_post

@app.delete("/posts/{post_id}", response_model=dict)
def delete_post(post_id: int, db: Session = Depends(get_db)):
    db_post = db.query(PostDB).filter(PostDB.id == post_id).first()
    if db_post is None:
        raise HTTPException(status_code=404, detail="Post not found")
    db.delete(db_post)
    db.commit()
    return {"ok": True}
Результат: после запуска сервера можно создавать, читать, обновлять и удалять посты через API.

Аутентификация с JWT (JSON Web Token)

Используем библиотеку python-jose и passlib для хеширования паролей. Создадим эндпоинты регистрации, логина и защищенный маршрут.

Пример

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
from datetime import datetime, timedelta

app = FastAPI()

SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"])
security = HTTPBearer()

fake_users_db = {"testuser": {"username": "testuser", "hashed_password": pwd_context.hash("password")}}

class Token(BaseModel):
    access_token: str
    token_type: str

class UserIn(BaseModel):
    username: str
    password: str

class UserOut(BaseModel):
    username: str

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

@app.post("/token", response_model=Token)
async def login(form_data: UserIn):
    user = fake_users_db.get(form_data.username)
    if not user or not pwd_context.verify(form_data.password, user["hashed_password"]):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
    access_token = create_access_token(data={"sub": form_data.username})
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/users/me", response_model=UserOut)
async def read_users_me(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Invalid token")
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")
    user = fake_users_db.get(username)
    if user is None:
        raise HTTPException(status_code=401, detail="User not found")
    return UserOut(**user)
Результат: POST /token возвращает токен, GET /users/me возвращает данные пользователя при предъявлении токена.

WebSocket приложение

FastAPI поддерживает WebSocket. Создадим простой чат-сервер с трансляцией сообщений.

Пример

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)

manager = ConnectionManager()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"User says: {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast("User disconnected")
Результат: после подключения к ws://localhost:8000/ws, все отправленные сообщения транслируются всем подключенным клиентам.

Создание приложения на FastAPI и Python - comments

En
Fastapi python приложение (python)