REST API з FastAPI: від нуля до продакшну
Створюємо сучасний, швидкий та документований API на Python
FastAPI — найшвидший Python-фреймворк для створення API. Автоматична документація, валідація даних, асинхронність з коробки.
Швидкість
На рівні Node.js та GoДокументація
Swagger/OpenAPI автоматичноВалідація
Pydantic на всіх рівняхType Hints
IDE autocompleteПочаток роботи
# Встановлення
pip install fastapi uvicorn[standard] pydantic sqlalchemy
# Структура проекту
my_api/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── routers/
│ │ ├── __init__.py
│ │ └── users.py
│ └── services/
│ ├── __init__.py
│ └── user_service.py
├── tests/
├── requirements.txt
└── docker-compose.yml
Базовий застосунок
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(
title="My API",
description="Курсова робота: REST API",
version="1.0.0",
docs_url="/docs", # Swagger UI
redoc_url="/redoc" # ReDoc
)
# CORS для frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "Welcome to My API"}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
# Запуск
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Відкрийте:
# http://localhost:8000/docs - Swagger UI
# http://localhost:8000/redoc - ReDoc
Pydantic: валідація даних
Pydantic — серце FastAPI. Автоматична валідація, серіалізація, документація.
# app/schemas/user.py
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
from datetime import datetime
from enum import Enum
class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
class UserBase(BaseModel):
"""Базова схема користувача."""
email: EmailStr
username: str = Field(
...,
min_length=3,
max_length=50,
description="Унікальне ім'я користувача"
)
full_name: Optional[str] = Field(
None,
max_length=100
)
@field_validator("username")
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("Username must be alphanumeric")
return v.lower()
class UserCreate(UserBase):
"""Схема для створення користувача."""
password: str = Field(
...,
min_length=8,
description="Пароль (мінімум 8 символів)"
)
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError("Password must contain uppercase")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain digit")
return v
class UserResponse(UserBase):
"""Схема відповіді (без пароля!)."""
id: int
role: UserRole
is_active: bool
created_at: datetime
class Config:
from_attributes = True # Для ORM моделей
class UserUpdate(BaseModel):
"""Схема для оновлення (все опціонально)."""
email: Optional[EmailStr] = None
full_name: Optional[str] = None
is_active: Optional[bool] = None
SQLAlchemy: робота з базою даних
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql://user:pass@localhost/mydb"
# Для SQLite: "sqlite:///./app.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Dependency для отримання сесії БД."""
db = SessionLocal()
try:
yield db
finally:
db.close()
# app/models/user.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum
from sqlalchemy.sql import func
from app.database import Base
import enum
class UserRole(str, enum.Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
username = Column(String(50), unique=True, index=True, nullable=False)
full_name = Column(String(100))
hashed_password = Column(String(255), nullable=False)
role = Column(Enum(UserRole), default=UserRole.USER)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
Router: CRUD операції
# app/routers/users.py
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserResponse, UserUpdate
from app.services.auth import get_password_hash
router = APIRouter(
prefix="/users",
tags=["users"]
)
@router.get("/", response_model=List[UserResponse])
async def get_users(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Отримання списку користувачів з пагінацією."""
users = db.query(User).offset(skip).limit(limit).all()
return users
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
db: Session = Depends(get_db)
):
"""Отримання користувача за ID."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
db: Session = Depends(get_db)
):
"""Створення нового користувача."""
# Перевірка унікальності
if db.query(User).filter(User.email == user_data.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Створення користувача
user = User(
email=user_data.email,
username=user_data.username,
full_name=user_data.full_name,
hashed_password=get_password_hash(user_data.password)
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UserUpdate,
db: Session = Depends(get_db)
):
"""Часткове оновлення користувача."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Оновлюємо лише передані поля
update_data = user_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
db.commit()
db.refresh(user)
return user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: int,
db: Session = Depends(get_db)
):
"""Видалення користувача."""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user)
db.commit()
JWT Аутентифікація
# app/services/auth.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.user import User
# Конфігурація
SECRET_KEY = "your-secret-key-here" # В продакшні: env variable!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(
data: dict,
expires_delta: Optional[timedelta] = None
) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> User:
"""Dependency для отримання поточного користувача."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user)
) -> User:
"""Перевірка що користувач активний."""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user
Використання в роутах
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: User = Depends(get_current_active_user)
):
"""Отримання інформації про поточного користувача."""
return current_user
@router.delete("/{user_id}", dependencies=[Depends(require_admin)])
async def delete_user(user_id: int, db: Session = Depends(get_db)):
"""Видалення користувача (тільки для адмінів)."""
...
Тестування API
# tests/test_users.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
# Тестова база даних
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
TestingSessionLocal = sessionmaker(bind=engine)
@pytest.fixture(scope="function")
def db():
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db):
def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
class TestUsers:
def test_create_user(self, client):
response = client.post(
"/users/",
json={
"email": "test@example.com",
"username": "testuser",
"password": "SecurePass123"
}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert "id" in data
assert "password" not in data # Пароль не повертається!
def test_create_user_duplicate_email(self, client):
# Перший користувач
client.post("/users/", json={
"email": "test@example.com",
"username": "user1",
"password": "SecurePass123"
})
# Дублікат email
response = client.post("/users/", json={
"email": "test@example.com",
"username": "user2",
"password": "SecurePass123"
})
assert response.status_code == 400
def test_get_user_not_found(self, client):
response = client.get("/users/999")
assert response.status_code == 404
# Запуск тестів
pytest tests/ -v --cov=app --cov-report=html
Потрібна допомога з проектом?
Створення production-ready REST API вимагає досвіду. Наші розробники допоможуть з архітектурою, реалізацією та документацією вашої курсової — без передоплати.
Замовити курсову з PythonІдеї для курсової роботи
Функціонал: Товари, категорії, кошик, замовлення, оплата (Stripe/LiqPay).
Технології: FastAPI, PostgreSQL, Redis (кеш), Celery (async tasks)
Складність: Середня
Функціонал: Проекти, завдання, коментарі, файли, сповіщення.
Технології: FastAPI, MongoDB, WebSockets (real-time)
Складність: Середня
Функціонал: Завантаження даних, inference, батч-обробка, моніторинг.
Технології: FastAPI, PyTorch/TensorFlow, Prometheus, Docker
Складність: Висока
Потрібна допомога з курсовою?
REST API — обов'язкова навичка для бекенд-розробника. Ми допоможемо з архітектурою, реалізацією та документацією.
Замовити курсову з FastAPI