Bir CLI aracı yazmak yeterli olmadığında sırada REST API var: herhangi bir frontend, mobil uygulama ya da başka bir servisin çağırabileceği bir web API’si.

Bu yazıda FastAPI kullanacağız; en hızlı büyüyen Python web framework’ü. Otomatik dokümantasyon, tip kontrolü ve yerleşik doğrulama (validation) sunuyor. Yazının sonunda çalışan bir Yer İmi Yöneticisi (Bookmark Manager) API’niz olacak.

Neden FastAPI?

  • Otomatik API dokümantasyonu: /docs adresinde Swagger UI (ücretsiz, ek kurulum gerekmez)
  • Tip güvenli: Doğrulama için Python type hint’lerini kullanır
  • Hızlı: Starlette ve Pydantic üzerine kurulu (en hızlı Python framework’lerinden biri)
  • Async desteği: Yerleşik async/await desteği
  • Pydantic doğrulama: Otomatik girdi doğrulama

Bağımlılıkları kurun:

pip install fastapi uvicorn sqlalchemy

Proje Yapısı

bookmark-api/
    src/
        py23_fastapi.py   # Modeller, şemalar, route'lar, app
    tests/
        test_py23.py      # TestClient ile API testleri

Adım 1: Veritabanı Modelleri (SQLAlchemy)

from sqlalchemy import String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
from sqlalchemy.pool import StaticPool

class Base(DeclarativeBase):
    pass

class BookmarkDB(Base):
    __tablename__ = "bookmarks"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    url: Mapped[str] = mapped_column(String(500))
    description: Mapped[str] = mapped_column(String(1000), default="")
    category: Mapped[str] = mapped_column(String(100), default="general")
    created_at: Mapped[str] = mapped_column(String(30), default="")

# Veritabanı kurulumu
DATABASE_URL = "sqlite:///./bookmarks.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)

Adım 2: Pydantic Şemaları (API Katmanı)

Şemalar, API’nin ne tür veri kabul edip döndürdüğünü tanımlıyor. FastAPI’nin asıl güçlü olduğu yer burası: girdiyi otomatik olarak doğruluyor.

from pydantic import BaseModel, Field

class BookmarkCreate(BaseModel):
    """İstemcinin bir yer imi oluştururken gönderdiği veri."""
    title: str = Field(min_length=1, max_length=200)
    url: str = Field(min_length=1, max_length=500)
    description: str = Field(default="", max_length=1000)
    category: str = Field(default="general", max_length=100)

class BookmarkUpdate(BaseModel):
    """İstemcinin bir yer imini güncellerken gönderdiği veri (hepsi opsiyonel)."""
    title: str | None = Field(default=None, min_length=1, max_length=200)
    url: str | None = Field(default=None, min_length=1, max_length=500)
    description: str | None = Field(default=None, max_length=1000)
    category: str | None = Field(default=None, max_length=100)

class BookmarkResponse(BaseModel):
    """API'nin döndürdüğü veri."""
    id: int
    title: str
    url: str
    description: str
    category: str
    created_at: str

    model_config = {"from_attributes": True}

class BookmarkListResponse(BaseModel):
    """Sayfalanmış yer imi listesi."""
    items: list[BookmarkResponse]
    total: int
    page: int
    per_page: int

Önemli kavramlar:

  • Field(min_length=1): FastAPI’nin otomatik uyguladığı doğrulama
  • BookmarkUpdate‘te tüm alanlar opsiyonel: kısmi güncellemeler için
  • model_config = {"from_attributes": True}: Pydantic’in SQLAlchemy nesnelerini doğrudan okumasını sağlar

Adım 3: Repository Deseni (CRUD)

Tüm veritabanı işlemlerini bir repository sınıfına koyuyoruz:

from datetime import datetime
from sqlalchemy.orm import Session

class BookmarkRepository:
    def __init__(self, session: Session) -> None:
        self.session = session

    def create(self, data: BookmarkCreate) -> BookmarkDB:
        bookmark = BookmarkDB(
            title=data.title,
            url=data.url,
            description=data.description,
            category=data.category,
            created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        )
        self.session.add(bookmark)
        self.session.commit()
        self.session.refresh(bookmark)
        return bookmark

    def get_by_id(self, bookmark_id: int) -> BookmarkDB | None:
        return self.session.get(BookmarkDB, bookmark_id)

    def get_all(
        self, page: int = 1, per_page: int = 20,
        category: str | None = None, search: str | None = None,
    ) -> tuple[list[BookmarkDB], int]:
        query = self.session.query(BookmarkDB)
        if category:
            query = query.filter(BookmarkDB.category == category)
        if search:
            query = query.filter(BookmarkDB.title.contains(search))
        total = query.count()
        items = query.offset((page - 1) * per_page).limit(per_page).all()
        return items, total

    def update(self, bookmark_id: int, data: BookmarkUpdate) -> BookmarkDB | None:
        bookmark = self.get_by_id(bookmark_id)
        if bookmark is None:
            return None
        for field_name, value in data.model_dump(exclude_unset=True).items():
            if value is not None:
                setattr(bookmark, field_name, value)
        self.session.commit()
        self.session.refresh(bookmark)
        return bookmark

    def delete(self, bookmark_id: int) -> bool:
        bookmark = self.get_by_id(bookmark_id)
        if bookmark is None:
            return False
        self.session.delete(bookmark)
        self.session.commit()
        return True

Adım 4: FastAPI Route’ları

Şimdi her şeyi FastAPI route’larıyla birbirine bağlıyoruz:

from fastapi import APIRouter, Depends, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
    title="Bookmark Manager API",
    description="A REST API for managing bookmarks",
    version="1.0.0",
)

# CORS: frontend uygulamalarının bu API'yi çağırmasına izin verir
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Üretimde "*" yerine kendi frontend domaininizi yazın
    allow_methods=["*"],
    allow_headers=["*"],
)

router = APIRouter()

Depends() ile Bağımlılık Enjeksiyonu

FastAPI’nin Depends() özelliği en güçlü yeteneklerinden biri. Kaynakları otomatik olarak oluşturup temizliyor:

def get_db():
    """Her istek için bir veritabanı oturumu oluştur."""
    db = SessionFactory()
    try:
        yield db  # İstek bu oturumu kullanır
    finally:
        db.close()  # İstek bitince temizle

def get_repo(db: Session = Depends(get_db)) -> BookmarkRepository:
    """İsteğin veritabanı oturumuyla bir repository oluştur."""
    return BookmarkRepository(db)

Artık veritabanına ihtiyaç duyan her route, repo: BookmarkRepository = Depends(get_repo) eklemesi yeterli.

CRUD Route’ları

@router.post("/bookmarks", response_model=BookmarkResponse, status_code=201)
def create_bookmark(
    data: BookmarkCreate,
    repo: BookmarkRepository = Depends(get_repo),
):
    """Yeni bir yer imi oluştur."""
    return repo.create(data)

@router.get("/bookmarks", response_model=BookmarkListResponse)
def list_bookmarks(
    page: int = Query(default=1, ge=1),
    per_page: int = Query(default=20, ge=1, le=100),
    category: str | None = None,
    search: str | None = None,
    repo: BookmarkRepository = Depends(get_repo),
):
    """Sayfalama ve filtreleme ile yer imlerini listele."""
    items, total = repo.get_all(page=page, per_page=per_page,
                                category=category, search=search)
    return BookmarkListResponse(
        items=items, total=total, page=page, per_page=per_page
    )

@router.get("/bookmarks/{bookmark_id}", response_model=BookmarkResponse)
def get_bookmark(
    bookmark_id: int,
    repo: BookmarkRepository = Depends(get_repo),
):
    """Tek bir yer imini getir."""
    bookmark = repo.get_by_id(bookmark_id)
    if bookmark is None:
        raise HTTPException(status_code=404, detail="Bookmark not found")
    return bookmark

@router.put("/bookmarks/{bookmark_id}", response_model=BookmarkResponse)
def update_bookmark(
    bookmark_id: int,
    data: BookmarkUpdate,
    repo: BookmarkRepository = Depends(get_repo),
):
    """Bir yer imini güncelle."""
    bookmark = repo.update(bookmark_id, data)
    if bookmark is None:
        raise HTTPException(status_code=404, detail="Bookmark not found")
    return bookmark

@router.delete("/bookmarks/{bookmark_id}", status_code=204)
def delete_bookmark(
    bookmark_id: int,
    repo: BookmarkRepository = Depends(get_repo),
):
    """Bir yer imini sil."""
    if not repo.delete(bookmark_id):
        raise HTTPException(status_code=404, detail="Bookmark not found")

app.include_router(router)

Önemli kalıplar:

  • response_model=BookmarkResponse: FastAPI yanıtı otomatik olarak serileştirir
  • Query(default=1, ge=1): Query parametrelerini doğrular (page en az 1 olmalı)
  • HTTPException(status_code=404): Doğru HTTP hata kodlarını döndürür
  • status_code=201: POST, 201 Created döndürür (200 OK değil)
  • status_code=204: DELETE, 204 No Content döndürür

Adım 5: API’yi Çalıştırma

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Çalıştırın:

python src/py23_fastapi.py

Otomatik oluşturulan Swagger dokümantasyonunu görmek için http://localhost:8000/docs adresini açın. Her endpoint’i doğrudan tarayıcıda test edebilirsiniz.

curl ile Test Etme

# Yer imi oluştur
curl -X POST http://localhost:8000/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"title": "Python Docs", "url": "https://docs.python.org", "category": "python"}'

# Tüm yer imlerini listele
curl http://localhost:8000/bookmarks

# Tek bir yer imini getir
curl http://localhost:8000/bookmarks/1

# Yer imini güncelle
curl -X PUT http://localhost:8000/bookmarks/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Python 3.13 Docs"}'

# Yer imini sil
curl -X DELETE http://localhost:8000/bookmarks/1

Adım 6: Test İçin App Factory

Test için ayrı bir veritabanıyla yeni bir app oluşturan bir factory fonksiyonuna ihtiyacımız var:

def create_app(database_url: str = DATABASE_URL) -> FastAPI:
    """Verilen veritabanı URL'siyle bir FastAPI app oluştur."""
    test_engine = create_engine(
        database_url,
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    TestSessionFactory = sessionmaker(
        autocommit=False, autoflush=False, bind=test_engine
    )
    Base.metadata.create_all(bind=test_engine)

    test_app = FastAPI(title="Bookmark Manager API", version="1.0.0")
    test_app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
    )

    def get_test_db():
        db = TestSessionFactory()
        try:
            yield db
        finally:
            db.close()

    def get_test_repo(db: Session = Depends(get_test_db)) -> BookmarkRepository:
        return BookmarkRepository(db)

    # Route'ları standalone router ile kaydet (app.router değil)
    test_app.include_router(router)
    test_app.dependency_overrides[get_db] = get_test_db
    test_app.dependency_overrides[get_repo] = get_test_repo
    return test_app

Factory, ayrı bir veritabanı motoru oluşturuyor ve bağımlılıkları override ediyor. Bu sayede testler, üretim dosyası yerine bellek içi (in-memory) bir SQLite veritabanı kullanabiliyor. Üretimde allow_origins=["*"] yerine frontend’inizin domainini yazın (örn. ["https://myapp.com"]).

Adım 7: pytest ile Test Yazma

API’yi FastAPI’nin TestClient‘ıyla test ediyoruz (arka planda httpx kullanıyor):

import pytest
from fastapi.testclient import TestClient
from src.py23_fastapi import create_app

@pytest.fixture
def client():
    app = create_app("sqlite:///:memory:")
    return TestClient(app)

class TestAPI:
    def test_create_bookmark(self, client):
        response = client.post("/bookmarks", json={
            "title": "Python Docs",
            "url": "https://docs.python.org",
        })
        assert response.status_code == 201
        assert response.json()["title"] == "Python Docs"

    def test_create_invalid(self, client):
        response = client.post("/bookmarks", json={"title": "", "url": ""})
        assert response.status_code == 422  # Doğrulama hatası

    def test_get_not_found(self, client):
        response = client.get("/bookmarks/999")
        assert response.status_code == 404

    def test_full_crud(self, client):
        # Oluştur
        r = client.post("/bookmarks", json={
            "title": "Test", "url": "https://test.com"
        })
        bm_id = r.json()["id"]

        # Oku
        r = client.get(f"/bookmarks/{bm_id}")
        assert r.json()["title"] == "Test"

        # Güncelle
        r = client.put(f"/bookmarks/{bm_id}", json={"title": "Updated"})
        assert r.json()["title"] == "Updated"

        # Sil
        r = client.delete(f"/bookmarks/{bm_id}")
        assert r.status_code == 204

        # Silindiğini doğrula
        r = client.get(f"/bookmarks/{bm_id}")
        assert r.status_code == 404

Testleri çalıştırın:

python -m pytest tests/test_py23.py -v

Sık Yapılan Hatalar

1. 404 Hatalarını Yönetmemek

# KÖTÜ: None döndürür, bu da null JSON'a dönüşür
@app.get("/bookmarks/{id}")
def get_bookmark(id: int):
    return repo.get_by_id(id)  # Bulunamazsa None!

# İYİ: uygun bir 404 döndür
@app.get("/bookmarks/{id}")
def get_bookmark(id: int):
    bookmark = repo.get_by_id(id)
    if bookmark is None:
        raise HTTPException(status_code=404, detail="Not found")
    return bookmark

2. Veritabanı Nesnelerini Doğrudan Döndürmek

# KÖTÜ: iç veritabanı yapısını dışarı sızdırır
@app.get("/bookmarks")
def list_bookmarks():
    return session.query(BookmarkDB).all()

# İYİ: çıktıyı kontrol etmek için response_model kullan
@app.get("/bookmarks", response_model=list[BookmarkResponse])
def list_bookmarks():
    return session.query(BookmarkDB).all()

3. Girdi Doğrulaması Yapmamak

# KÖTÜ: herhangi bir string'i kabul eder
class BookmarkCreate(BaseModel):
    title: str
    url: str

# İYİ: uzunluk ve formatı doğrular
class BookmarkCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    url: str = Field(min_length=1, max_length=500)

FastAPI’nin Türkiye’deki Yeri: Veri Bilimi ve Yapay Zeka Bootcamp’leri

FastAPI’nin Türkiye’deki en görünür kullanım alanı klasik web backend’lerinden çok, veri bilimi ve yapay zeka tarafı. Python zaten veri bilimi ve makine öğrenmesi ekosisteminin ana dili; bir model eğitildikten sonra onu bir uygulamaya bağlamanın en doğal yolu, aynı dilde kalıp FastAPI ile bir API açmak. Bu yüzden Türkiye’deki veri bilimi ve yapay zeka odaklı bootcamp müfredatlarında FastAPI, “modelinizi nasıl servis haline getirirsiniz” sorusunun standart cevabı haline geldi: Flask’a göre daha az kod yazdırıyor, Pydantic sayesinde girdi doğrulamasını neredeyse bedavaya veriyor ve /docs üzerinden otomatik oluşan arayüz, ekip içi demo ya da hızlı entegrasyon testleri için ekstra bir araç kurmadan yetiyor.

Üretime taşırken akılda tutulması gereken pratik bir nokta: yukarıdaki uvicorn.run() komutu geliştirme için yeterli, ama gerçek trafik için önüne bir process manager (Gunicorn + Uvicorn worker’ları gibi) ve ideal olarak bir konteyner koymanız gerekiyor. Bu adımı Docker Nasıl Kurulur? yazısından takip edebilirsiniz.

İlgili Yazılar