Разработка веб приложений на 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, все отправленные сообщения транслируются всем подключенным клиентам.