بناء واجهات REST API جاهزة للإنتاج باستخدام FastAPI و PostgreSQL و Docker

FastAPI هو الإطار الأسرع نمواً في عالم بايثون — ولسبب وجيه. توثيق تلقائي، أمان الأنواع، دعم البرمجة غير المتزامنة، وأداء خارق. في هذا الدرس، ستبني واجهة REST API متكاملة من الصفر وتحزّمها في حاويات Docker جاهزة للإنتاج.
ماذا ستبني؟
نظام إدارة مهام (TaskFlow) متكامل يتضمّن:
- عمليات CRUD كاملة للمشاريع والمهام
- مصادقة المستخدمين بتقنية JWT
- قاعدة بيانات PostgreSQL مع SQLAlchemy ORM
- ترحيل قاعدة البيانات (Migrations) باستخدام Alembic
- بيئة Docker Compose للتطوير والإنتاج
- توثيق تلقائي للـ API عبر Swagger و ReDoc
- تحقق من المدخلات ومعالجة الأخطاء
- إعدادات مرنة حسب البيئة
المتطلبات المسبقة
قبل البدء، تأكد من توفر التالي:
- Python 3.11+ (python.org)
- Docker و Docker Compose (docker.com)
- معرفة أساسية ببايثون وواجهات REST API
- محرر أكواد (يُنصح بـ VS Code)
- سطر أوامر (Terminal)
جميع الأكواد في هذا الدرس متاحة على GitHub. يمكنك المتابعة خطوة بخطوة أو استنساخ المشروع النهائي مباشرة.
الخطوة 1: إعداد المشروع وهيكلة الملفات
أنشئ مجلد المشروع ورتّب الملفات:
mkdir taskflow-api && cd taskflow-api
mkdir -p app/{api,core,models,schemas,services}
touch app/__init__.py app/main.py app/database.py
touch app/api/__init__.py app/core/__init__.py
touch app/models/__init__.py app/schemas/__init__.py
touch app/services/__init__.py
touch requirements.txt .env .env.exampleهيكلة المشروع:
taskflow-api/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── database.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── projects.py
│ │ └── tasks.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── security.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── schemas.py
│ └── services/
│ ├── __init__.py
│ └── auth_service.py
├── alembic/
├── tests/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── .env
└── alembic.ini
الخطوة 2: تثبيت المكتبات
أضف المكتبات التالية في ملف requirements.txt:
fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy==2.0.36
asyncpg==0.30.0
alembic==1.14.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.12
pydantic-settings==2.6.0
httpx==0.28.0
pytest==8.3.0
pytest-asyncio==0.24.0أنشئ بيئة افتراضية وثبّت المكتبات:
python -m venv venv
source venv/bin/activate # على Windows: venv\Scripts\activate
pip install -r requirements.txtالخطوة 3: إدارة الإعدادات
أنشئ ملف app/core/config.py لإدارة متغيرات البيئة:
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
# التطبيق
APP_NAME: str = "TaskFlow API"
APP_VERSION: str = "1.0.0"
DEBUG: bool = False
# قاعدة البيانات
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/taskflow"
# المصادقة
SECRET_KEY: str = "your-secret-key-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# CORS
ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
case_sensitive = True
@lru_cache()
def get_settings() -> Settings:
return Settings()أنشئ ملف .env:
APP_NAME=TaskFlow API
DEBUG=true
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/taskflow
SECRET_KEY=your-super-secret-key-at-least-32-characters-longلا ترفع أبداً ملف .env إلى مستودع الأكواد. أضفه فوراً إلى .gitignore. استخدم .env.example بقيم وهمية للتوثيق.
الخطوة 4: إعداد قاعدة البيانات مع SQLAlchemy
أنشئ الاتصال غير المتزامن في app/database.py:
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase
from app.core.config import get_settings
settings = get_settings()
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_size=20,
max_overflow=10,
pool_pre_ping=True,
)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
class Base(DeclarativeBase):
pass
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()لاحظ استخدام pool_pre_ping=True الذي يتأكد من صلاحية الاتصال قبل كل استعلام — مهم جداً في بيئة الإنتاج لتجنّب الاتصالات المنقطعة.
الخطوة 5: تعريف النماذج (Models)
أنشئ app/models/models.py:
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
String,
Text,
Enum as SAEnum,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
import enum
class TaskStatus(str, enum.Enum):
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
ARCHIVED = "archived"
class TaskPriority(str, enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
email: Mapped[str] = mapped_column(
String(255), unique=True, index=True, nullable=False
)
username: Mapped[str] = mapped_column(
String(100), unique=True, index=True, nullable=False
)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
# العلاقات
projects: Mapped[list["Project"]] = relationship(back_populates="owner")
class Project(Base):
__tablename__ = "projects"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
owner_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# العلاقات
owner: Mapped["User"] = relationship(back_populates="projects")
tasks: Mapped[list["Task"]] = relationship(
back_populates="project", cascade="all, delete-orphan"
)
class Task(Base):
__tablename__ = "tasks"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
title: Mapped[str] = mapped_column(String(300), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[TaskStatus] = mapped_column(
SAEnum(TaskStatus), default=TaskStatus.TODO
)
priority: Mapped[TaskPriority] = mapped_column(
SAEnum(TaskPriority), default=TaskPriority.MEDIUM
)
due_date: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
project_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
# العلاقات
project: Mapped["Project"] = relationship(back_populates="tasks")نستخدم Mapped مع mapped_column وهو الأسلوب الحديث في SQLAlchemy 2.0 الذي يوفر دعماً كاملاً للأنواع (Type Hints). هذا يعني أن محرر الأكواد سيُعطيك اقتراحات وتحذيرات دقيقة.
الخطوة 6: مخططات Pydantic للتحقق من البيانات
أنشئ app/schemas/schemas.py:
import uuid
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field
from app.models.models import TaskStatus, TaskPriority
# --- مخططات المصادقة ---
class UserCreate(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=100)
password: str = Field(..., min_length=8)
class UserResponse(BaseModel):
id: uuid.UUID
email: str
username: str
is_active: bool
created_at: datetime
model_config = {"from_attributes": True}
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class TokenData(BaseModel):
user_id: uuid.UUID | None = None
# --- مخططات المشاريع ---
class ProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
description: str | None = None
class ProjectUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=200)
description: str | None = None
class ProjectResponse(BaseModel):
id: uuid.UUID
name: str
description: str | None
owner_id: uuid.UUID
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
# --- مخططات المهام ---
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=300)
description: str | None = None
status: TaskStatus = TaskStatus.TODO
priority: TaskPriority = TaskPriority.MEDIUM
due_date: datetime | None = None
class TaskUpdate(BaseModel):
title: str | None = Field(None, min_length=1, max_length=300)
description: str | None = None
status: TaskStatus | None = None
priority: TaskPriority | None = None
due_date: datetime | None = None
class TaskResponse(BaseModel):
id: uuid.UUID
title: str
description: str | None
status: TaskStatus
priority: TaskPriority
due_date: datetime | None
project_id: uuid.UUID
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}الفصل بين مخططات الإدخال (Create/Update) ومخططات الإخراج (Response) أمر جوهري. لا تريد أبداً كشف hashed_password في الاستجابة!
الخطوة 7: المصادقة بتقنية JWT
أنشئ app/core/security.py:
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import get_settings
from app.database import get_db
from app.models.models import User
from app.schemas.schemas import TokenData
settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
token_data = TokenData(user_id=user_id)
except JWTError:
raise credentials_exception
result = await db.execute(
select(User).where(User.id == token_data.user_id)
)
user = result.scalar_one_or_none()
if user is None:
raise credentials_exception
return userالجميل في Depends هو أن FastAPI يبني لك سلسلة حقن التبعيات (Dependency Injection) تلقائياً. عندما تضع get_current_user كمعامل في أي نقطة نهاية، سيتحقق FastAPI من التوكن ويجلب المستخدم قبل تنفيذ الدالة.
الخطوة 8: مسارات الـ API
مسارات المصادقة — app/api/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.models import User
from app.schemas.schemas import UserCreate, UserResponse, Token
from app.core.security import hash_password, verify_password, create_access_token
router = APIRouter(prefix="/api/auth", tags=["المصادقة"])
@router.post("/register", response_model=UserResponse, status_code=201)
async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
# التحقق من عدم وجود البريد مسبقاً
result = await db.execute(select(User).where(User.email == user_data.email))
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="البريد الإلكتروني مسجل مسبقاً",
)
# التحقق من عدم وجود اسم المستخدم
result = await db.execute(
select(User).where(User.username == user_data.username)
)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="اسم المستخدم محجوز",
)
user = User(
email=user_data.email,
username=user_data.username,
hashed_password=hash_password(user_data.password),
)
db.add(user)
await db.flush()
await db.refresh(user)
return user
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(User).where(User.email == form_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="البريد أو كلمة المرور غير صحيحة",
)
access_token = create_access_token(data={"sub": str(user.id)})
return Token(access_token=access_token)مسارات المشاريع — app/api/projects.py
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.models import Project, User
from app.schemas.schemas import ProjectCreate, ProjectUpdate, ProjectResponse
from app.core.security import get_current_user
router = APIRouter(prefix="/api/projects", tags=["المشاريع"])
@router.get("/", response_model=list[ProjectResponse])
async def list_projects(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Project).where(Project.owner_id == current_user.id)
)
return result.scalars().all()
@router.post("/", response_model=ProjectResponse, status_code=201)
async def create_project(
project_data: ProjectCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
project = Project(**project_data.model_dump(), owner_id=current_user.id)
db.add(project)
await db.flush()
await db.refresh(project)
return project
@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project(
project_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Project).where(
Project.id == project_id,
Project.owner_id == current_user.id,
)
)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="المشروع غير موجود")
return project
@router.patch("/{project_id}", response_model=ProjectResponse)
async def update_project(
project_id: UUID,
project_data: ProjectUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Project).where(
Project.id == project_id,
Project.owner_id == current_user.id,
)
)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="المشروع غير موجود")
update_data = project_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(project, field, value)
await db.flush()
await db.refresh(project)
return project
@router.delete("/{project_id}", status_code=204)
async def delete_project(
project_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Project).where(
Project.id == project_id,
Project.owner_id == current_user.id,
)
)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="المشروع غير موجود")
await db.delete(project)مسارات المهام — app/api/tasks.py
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.models import Task, Project, User, TaskStatus
from app.schemas.schemas import TaskCreate, TaskUpdate, TaskResponse
from app.core.security import get_current_user
router = APIRouter(prefix="/api/projects/{project_id}/tasks", tags=["المهام"])
@router.get("/", response_model=list[TaskResponse])
async def list_tasks(
project_id: UUID,
status: TaskStatus | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
proj = await db.execute(
select(Project).where(
Project.id == project_id,
Project.owner_id == current_user.id,
)
)
if not proj.scalar_one_or_none():
raise HTTPException(status_code=404, detail="المشروع غير موجود")
query = select(Task).where(Task.project_id == project_id)
if status:
query = query.where(Task.status == status)
result = await db.execute(query.order_by(Task.created_at.desc()))
return result.scalars().all()
@router.post("/", response_model=TaskResponse, status_code=201)
async def create_task(
project_id: UUID,
task_data: TaskCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
proj = await db.execute(
select(Project).where(
Project.id == project_id,
Project.owner_id == current_user.id,
)
)
if not proj.scalar_one_or_none():
raise HTTPException(status_code=404, detail="المشروع غير موجود")
task = Task(**task_data.model_dump(), project_id=project_id)
db.add(task)
await db.flush()
await db.refresh(task)
return task
@router.patch("/{task_id}", response_model=TaskResponse)
async def update_task(
project_id: UUID,
task_id: UUID,
task_data: TaskUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Task)
.join(Project)
.where(
Task.id == task_id,
Task.project_id == project_id,
Project.owner_id == current_user.id,
)
)
task = result.scalar_one_or_none()
if not task:
raise HTTPException(status_code=404, detail="المهمة غير موجودة")
for field, value in task_data.model_dump(exclude_unset=True).items():
setattr(task, field, value)
await db.flush()
await db.refresh(task)
return task
@router.delete("/{task_id}", status_code=204)
async def delete_task(
project_id: UUID,
task_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Task)
.join(Project)
.where(
Task.id == task_id,
Task.project_id == project_id,
Project.owner_id == current_user.id,
)
)
task = result.scalar_one_or_none()
if not task:
raise HTTPException(status_code=404, detail="المهمة غير موجودة")
await db.delete(task)الخطوة 9: نقطة الدخول الرئيسية
أنشئ app/main.py:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import get_settings
from app.api import auth, projects, tasks
settings = get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
# عند بدء التشغيل
print(f"🚀 {settings.APP_NAME} v{settings.APP_VERSION} جاري التشغيل...")
yield
# عند الإيقاف
print("👋 جاري الإيقاف...")
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# تسجيل المسارات
app.include_router(auth.router)
app.include_router(projects.router)
app.include_router(tasks.router)
@app.get("/health")
async def health_check():
return {"status": "healthy", "version": settings.APP_VERSION}الخطوة 10: ترحيل قاعدة البيانات مع Alembic
هيّئ Alembic واضبطه للعمل بشكل غير متزامن:
alembic init alembicعدّل ملف alembic/env.py لدعم async:
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from app.database import Base
from app.models.models import User, Project, Task # استيراد جميع النماذج
from app.core.config import get_settings
config = context.config
settings = get_settings()
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()أنشئ أول ترحيل وطبّقه:
alembic revision --autogenerate -m "initial tables"
alembic upgrade headالخطوة 11: حاويات Docker للمشروع
أنشئ Dockerfile:
FROM python:3.12-slim AS base
WORKDIR /app
# تثبيت المتطلبات النظامية
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
# تثبيت مكتبات بايثون
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# نسخ الكود
COPY . .
# مرحلة الإنتاج
FROM base AS production
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]أنشئ docker-compose.yml:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: taskflow
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
build:
context: .
target: production
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/taskflow
SECRET_KEY: ${SECRET_KEY:-change-me-in-production-please}
DEBUG: "false"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
migrate:
build:
context: .
target: base
command: alembic upgrade head
environment:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/taskflow
depends_on:
db:
condition: service_healthy
volumes:
postgres_data:شغّل كل شيء:
docker compose up -dانتظر لحظة ثم تحقق:
# تحقق من تشغيل الحاويات
docker compose ps
# تحقق من صحة الـ API
curl http://localhost:8000/healthستحصل على:
{"status": "healthy", "version": "1.0.0"}افتح http://localhost:8000/docs لترى واجهة Swagger التفاعلية المولّدة تلقائياً. يمكنك اختبار جميع نقاط النهاية مباشرة من المتصفح!
الخطوة 12: اختبار الـ API
أنشئ tests/test_api.py:
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.mark.anyio
async def test_health_check(client: AsyncClient):
response = await client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
@pytest.mark.anyio
async def test_register_and_login(client: AsyncClient):
# تسجيل حساب جديد
response = await client.post(
"/api/auth/register",
json={
"email": "test@example.com",
"username": "testuser",
"password": "securepassword123",
},
)
assert response.status_code == 201
assert response.json()["email"] == "test@example.com"
# تسجيل الدخول
login_response = await client.post(
"/api/auth/login",
data={"username": "test@example.com", "password": "securepassword123"},
)
assert login_response.status_code == 200
assert "access_token" in login_response.json()شغّل الاختبارات:
pytest tests/ -vالخطوة 13: نصائح لتجهيز الإنتاج
قبل النشر في بيئة الإنتاج، أضف هذه التحسينات:
1. تحديد عدد الطلبات (Rate Limiting)
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@router.post("/login")
@limiter.limit("5/minute")
async def login(request: Request, ...):
...2. السجلات المهيكلة (Structured Logging)
import structlog
logger = structlog.get_logger()
@app.middleware("http")
async def log_requests(request, call_next):
logger.info("request_started", method=request.method, path=request.url.path)
response = await call_next(request)
logger.info("request_completed", status_code=response.status_code)
return response3. فحص صحة التطبيق مع قاعدة البيانات
@app.get("/health")
async def health_check(db: AsyncSession = Depends(get_db)):
try:
await db.execute(text("SELECT 1"))
return {"status": "healthy", "database": "connected"}
except Exception:
return JSONResponse(
status_code=503,
content={"status": "unhealthy", "database": "disconnected"},
)الخلاصة
في هذا الدرس، بنيتَ واجهة REST API متكاملة وجاهزة للإنتاج تشمل:
- ✅ FastAPI — إطار بايثون غير متزامن مع توثيق OpenAPI تلقائي
- ✅ SQLAlchemy 2.0 — ORM حديث مع دعم كامل للأنواع
- ✅ PostgreSQL — قاعدة بيانات علائقية متينة
- ✅ Alembic — ترحيل قاعدة البيانات بتحكم إصداري
- ✅ JWT — مصادقة آمنة بالتوكنات
- ✅ Docker Compose — حاويات للتطوير والنشر
- ✅ Pydantic v2 — تحقق سريع من البيانات
- ✅ اختبارات — اختبارات غير متزامنة مع httpx و pytest
الخطوات التالية
- أضف ترقيم الصفحات (Pagination) لنقاط عرض القوائم
- طبّق WebSocket لتحديثات المهام الفورية
- أعدّ CI/CD مع GitHub Actions
- أضف تخزين مؤقت مع Redis
- انشر على خدمة سحابية (AWS ECS أو GCP Cloud Run أو خادم VPS مع Docker)
التوثيق التلقائي لـ FastAPI في /docs و /redoc يجعل التعاون مع فرق الواجهة الأمامية سهلاً للغاية — لا حاجة لكتابة توثيق منفصل.
برمجة سعيدة! 🚀
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

نشر تطبيق Next.js باستخدام Docker و CI/CD في بيئة الإنتاج
تعلم كيف تُحاوِل تطبيق Next.js باستخدام Docker، وتُعدّ خط أنابيب CI/CD مع GitHub Actions، وتنشر تلقائياً في بيئة الإنتاج على خادم VPS. دليل شامل من التطوير إلى النشر الآلي.

بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج
تعلّم كيفية بناء تطبيق متكامل آمن الأنواع باستخدام Drizzle ORM مع Next.js 15. يغطي هذا الدليل العملي تصميم المخططات والترحيلات وServer Actions وعمليات CRUD والنشر مع PostgreSQL.

بناء واجهات REST API باستخدام Rust و Axum: دليل عملي للمبتدئين
تعلّم كيف تبني واجهات REST API سريعة وآمنة باستخدام لغة Rust وإطار Axum. يغطي هذا الدليل خطوة بخطوة إعداد المشروع، التوجيه، معالجة JSON، الربط بقاعدة البيانات عبر SQLx، معالجة الأخطاء، والاختبارات — من الصفر إلى واجهة API عاملة.