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:
/docsadresinde 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ğrulamaBookmarkUpdate‘te tüm alanlar opsiyonel: kısmi güncellemeler içinmodel_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ştirirQuery(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ürstatus_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.