Python FastAPI REST Backend

REST API з FastAPI: від нуля до продакшну

Створюємо сучасний, швидкий та документований API на Python

28 лютого 2026 | 30 хв читання

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

Потрібна допомога з роботою?

Замовте професійне виконання — без передоплати, оплата після демонстрації!

Курсова з Python Курсова: Веб-додаток Курсова: Бази даних