Verdiğiniz eğitim içeriğini satır satır inceleyerek, belirttiğiniz kurallar doğrultusunda (dakika/saniye bilgileri, "Önizleme" ve "ders" satırları, anlamsız ifadeler temizlenerek) detaylı bir kaynak dokümana dönüştürdüm.
İlk olarak "Öğrenecekleriniz" bölümünün kapsamlı bir açıklamasını yapıp ardından müfredattaki tüm ana konuları derinlemesine, teorik anlatımlar, kod tasarımları ve pratik uygulama örnekleriyle açıkladım.
Bu eğitim serisi, modern yazılım dünyasının en kritik bileşenlerinden biri olan API (Uygulama Programlama Arayüzü) kavramını sıfırdan ele almayı hedefler. Katılımcılara sadece kod yazmayı değil, modern mimarilerin temelini oluşturan REST ilkelerini ve internetin iletişim dili olan HTTP metotlarını (GET, POST, PUT, DELETE) öğretmeyi amaçlar.
Eğitimin ana odağı olan FastAPI, Python dünyasındaki en performanslı ve modern asenkron framework'tür. Kurs boyunca bir yazılımcının production (canlı sistem) ortamında ihtiyaç duyacağı tüm modern yetkinlikler adım adım aktarılmaktadır:
Veri Yönetimi ve Güvenlik: İstemciden gelen verilerin Pydantic ile otomatik doğrulanması, veri tiplerinin kontrol edilmesi ve hatalı isteklerin daha sunucuya yük getirmeden engellenmesi işlenir.
Veritabanı Entegrasyonu: Verilerin kalıcı hafızada saklanması için SQLAlchemy ORM (Nesne İlişkisel Eşleme) aracı kullanılarak, ham SQL sorguları yazmaya gerek kalmadan Python nesneleriyle veritabanı yönetimi (CRUD işlemleri) öğretilir.
Endüstri Standartlarında Güvenlik: Sadece API yapmakla kalınmayıp, kullanıcı şifrelerinin bcrypt algoritmasıyla güvenli şekilde hash'lenmesi, oturum yönetimi için JWT (JSON Web Tokens) üretilmesi, süreli token mekanizmaları ve yetkisiz erişimleri engelleyen korumalı rotalar (Protected Routes) mimarisi kurulur.
Dokümantasyon ve Canlıya Alma (Deployment): FastAPI'nin en güçlü yanlarından biri olan otomatik Swagger UI dokümantasyonu sayesinde, yazılan API'lerin anında tarayıcı üzerinden nasıl test edileceği gösterilir. Son aşamada ise geliştirilen bu projelerin AWS, Railway, Render veya Heroku gibi bulut platformlarında dünya çapında erişime nasıl açılacağı uygulamalı olarak aktarılır.
Açıklama: API (Application Programming Interface), iki farklı yazılımın veya cihazın birbiriyle güvenli ve kurallı bir şekilde iletişim kurmasını sağlayan bir köprüdür. Örneğin; telefonunuzdaki hava durumu uygulaması, Meteoroloji Genel Müdürlüğü'nün sunucularına bir API isteği gönderir ve oradan aldığı güncel verileri size görsel olarak sunar.
İstemci (Client): İsteği başlatan taraf (Web tarayıcısı, mobil uygulama).
Sunucu (Server): İsteği karşılayan, veritabanı işlemlerini yürüten ve cevabı (Response) dönen taraf.
Açıklama: REST (Representational State Transfer), web servisleri tasarlanırken uyulması gereken bir mimari felsefedir. REST mimarisinde her şey bir kaynaktır (Resource) (Örn: Kullanıcılar, Ürünler, Makaleler). Bu kaynaklar üzerinde işlem yapmak için standart HTTP metotları kullanılır:
GET:
Sunucudan veri okumak/getirmek için kullanılır. Sunucu üzerinde
bir değişiklik yapmaz.
POST:
Sunucuda yeni bir kaynak oluşturmak için kullanılır. (Örn: Yeni
üye kaydı).
PUT:
Sunucudaki mevcut bir kaynağı tamamen güncellemek/değiştirmek
için kullanılır.
DELETE:
Sunucudaki bir kaynağı silmek için kullanılır.
Açıklama: FastAPI; Python
tabanlı, asenkron (async/await)
destekli, yüksek performanslı ve modern bir mikro-web
framework'üdür.
Flask/Django Farkı: Flask senkrondur ve veri doğrulaması için ek kütüphaneler ister. Django çok büyüktür ve REST API için ağır kalabilir. FastAPI ise NodeJS ve Go hızına yakın performans sunar, veri doğrulamasını yerleşik olarak Pydantic ile yapar ve kod yazıldığı an dokümantasyonu hazırlar.
İlk FastAPI projemizi ayağa kaldıralım.
Kurulum Komutları:
pip install fastapi uvicorn
Uygulama Kodu (main.py):
fromfastapiimportFastAPI# FastAPI nesnesini başlatıyoruzapp= FastAPI(title="İlk API Sunucum")# Kök (Root) dizine yapılan GET isteğini yakalıyoruz@app.get("/")defana_sayfa():return{"mesaj":"Merhaba Dünya! FastAPI sunucunuz başarıyla çalışıyor."}
Sunucuyu Başlatma Komutu:
uvicorn main:app --reload
--reload
parametresi, kodda bir değişiklik yaptığınızda sunucunun
otomatik olarak yeniden başlamasını sağlar.
Açıklama: Belirli tek bir
kaynağa erişmek için URL rotasının içerisine dinamik olarak
yerleştirilen değişkenlerdir. Örneğin; ://magaza.com
adresindeki 45
bir yol parametresidir ve 45 id'li ürünü temsil eder.
Açıklama: URL'in sonuna ?
işaretinden sonra eklenen, verileri filtrelemek, sıralamak veya
sayfalamak için kullanılan anahtar-değer çiftleridir. Örneğin;
://magaza.com
adresinde kategori
ve sirala
birer sorgu parametresidir.
Açıklama: POST
veya PUT
isteklerinde sunucuya gönderilecek karmaşık veriler URL üzerinden
gönderilemez; HTTP İstek Gövdesinde (Request Body) JSON formatında
gönderilir. Pydantic,
bu gelen JSON verisinin şemasını çıkartmamızı, veri tiplerini
(int, string, email vb.) otomatik doğrulamamızı sağlar.
Bellek üzerinde (In-Memory) çalışan, yol parametresi, sorgu parametresi ve Pydantic modeli içeren tam bir ürün yönetim API'si tasarlayalım.
fromfastapiimportFastAPI,HTTPException,statusfrompydanticimportBaseModel,FieldfromtypingimportOptionalapp= FastAPI()# Sahte Veritabanı (Bellek Üstünde)URUNLER_DB= {1: {"isim":"Kablosuz Kulaklık","fiyat":1250.0,"stokta_mi":True},2: {"isim":"Akıllı Saat","fiyat":3400.0,"stokta_mi":True}}# Pydantic Şeması (Veri Doğrulama için)classUrunSema(BaseModel):isim: str = Field(..., min_length=2, max_length=50)fiyat: float = Field(..., gt=0, description="Fiyat sıfırdan büyük olmalıdır.")stokta_mi: Optional[bool] =True# 1. READ ALL (Sorgu Parametresi Filtreli GET)@app.get("/urunler")defurunleri_listele(min_fiyat: Optional[float] =None):ifmin_fiyat:filtrelenmiş= {k: vfork,vinURUNLER_DB.items()ifv["fiyat"] >= min_fiyat}returnfiltrelenmişreturnURUNLER_DB# 2. READ ONE (Yol Parametreli GET)@app.get("/urunler/{urun_id}")defurun_getir(urun_id: int):ifurun_idnotinURUNLER_DB:raiseHTTPException(status_code=404, detail="Ürün bulunamadı!")returnURUNLER_DB[urun_id]# 3. CREATE (Request Body içeren POST)@app.post("/urunler", status_code=status.HTTP_201_CREATED)defurun_olustur(urun: UrunSema):yeni_id= max(URUNLER_DB.keys()) +1ifURUNLER_DBelse1URUNLER_DB[yeni_id] = urun.model_dump()return{"mesaj":"Ürün başarıyla eklendi","id": yeni_id,"veri": URUNLER_DB[yeni_id]}# 4. DELETE (DELETE)@app.delete("/urunler/{urun_id}")defurun_sil(urun_id: int):ifurun_idnotinURUNLER_DB:raiseHTTPException(status_code=404, detail="Silinmek istenen ürün mevcut değil.")delURUNLER_DB[urun_id]return{"mesaj":f"ID'si{urun_id}olan ürün başarıyla silindi."}
Açıklama: Bu modülde istemci sisteme bir PDF/Metin formatında özgeçmiş yükler. FastAPI bu dosyayı alır, metne dönüştürür ve OpenAI veya Google Gemini API'lerine bir "Prompt" ile gönderir. Gelen yapılandırılmış analiz sonuçlarını (Adayın yetenekleri, tecrübe yılı, uygunluk skoru) kullanıcıya JSON formatında döner.
Temel düzeyde bir OpenAI entegrasyonu sağlayan endpoint tasarımı yapalım.
fromfastapiimportFastAPI,UploadFile,File,HTTPExceptionimportosapp= FastAPI(title="AI Resume Parser")# Gerçek projede 'pip install openai' kurulmalı ve API Key tanımlanmalıdır.# Örnek simülasyon amaçlı mock AI fonksiyonu yazıyoruz.defyapay_zeka_analiz_et(metin: str):# Burada normal şartlarda OpenAI API çağrısı yapılır.if"Python"inmetinor"FastAPI"inmetin:return{"skor":95,"uygunluk":"Yüksek","Eksiklikler":"Yok"}return{"skor":40,"uygunluk":"Düşük","Eksiklikler":"Backend tecrübesi eksik"}@app.post("/resume/analyze")asyncdefresume_analiz_et(file: UploadFile = File(...)):# Dosya uzantı kontrolüifnotfile.filename.endswith('.txt'):raiseHTTPException(status_code=400, detail="Şimdilik sadece .txt formatındaki CV'ler desteklenmektedir.")# Dosya içeriğini okumaicerik=awaitfile.read()cv_metni= icerik.decode("utf-8")# Yapay zekaya gönderim ve sonuçanaliz_sonucu= yapay_zeka_analiz_et(cv_metni)return{"dosya_adi": file.filename,"analiz": analiz_sonucu}
Açıklama: Klasik
yöntemlerde veritabanı işlemleri için SELECT
* FROM kullanıcılar
gibi SQL cümleleri yazılır. ORM mimarisinde ise veritabanındaki
her bir tabloya karşılık gelen bir Python sınıfı (Model)
tanımlanır. Veri eklemek için sadece o sınıftan bir nesne üretip
kaydetmek yeterlidir. Eğitimde hafif, ayar gerektirmeyen yerel bir
veritabanı olan SQLite
kullanılmaktadır.
Kurulum:
pip install sqlalchemy
Veritabanı Altyapısı (database.py):
fromsqlalchemyimportcreate_engine,Column,Integer,String,Booleanfromsqlalchemy.ext.declarativeimportdeclarative_basefromsqlalchemy.ormimportsessionmakerSQLITE_URL="sqlite:///./test_veritabani.db"engine= create_engine(SQLITE_URL, connect_args={"check_same_thread":False})SessionLocal= sessionmaker(autocommit=False, autoflush=False, bind=engine)Base= declarative_base()# Bağımlılık Enjeksiyonu (Dependency Injection) için Veritabanı Session fonksiyonudefget_db():db= SessionLocal()try:yielddbfinally:db.close()
Veritabanı Modeli ve API Entegrasyonu (app.py):
fromfastapiimportFastAPI,Depends,HTTPExceptionfromsqlalchemy.ormimportSessionimportdatabase# Yukarıda yazdığımız dosya# Tabloyu TanımlayalımclassKullaniciModel(database.Base):__tablename__="kullanicilar"id= database.Column(database.Integer, primary_key=True, index=True)isim= database.Column(database.String)= database.Column(database.String, unique=True)# Tabloları fiziksel dosyada oluşturalımdatabase.Base.metadata.create_all(bind=database.engine)app= FastAPI()# Pydantic Girdi ŞemasıfrompydanticimportBaseModelclassKullaniciOlusturSema(BaseModel):isim: str: str@app.post("/kullanici-ekle")defveritabanina_yaz(kullanici: KullaniciOlusturSema,db: Session = Depends(database.get_db)):# Email kontrolümevcut= db.query(KullaniciModel).filter(KullaniciModel.email == kullanici.email).first()ifmevcut:raiseHTTPException(status_code=400, detail="Bu email zaten kayıtlı.")# ORM objesi oluşturma ve kaydetmeyeni_kullanici= KullaniciModel(isim=kullanici.isim, email=kullanici.email)db.add(yeni_kullanici)db.commit()db.refresh(yeni_kullanici)return{"mesaj":"Kullanıcı veritabanına kaydedildi","id": yeni_kullanici.id}
Açıklama: Kullanıcı şifreleri veritabanında asla "123456" gibi açık metin (plain text) olarak saklanamaz. Siber saldırılarda veritabanı ele geçirilse bile şifrelerin çözülememesi gerekir. Bcrypt, şifreyi geri döndürülemez matematiksel bir karmaşaya (hash) çevirir.
Açıklama: Kullanıcı sisteme başarılı bir şekilde giriş yaptığında (Login), sunucu ona şifrelenmiş, içinde kullanıcının ID'sini ve token'ın son kullanma tarihini barındıran bir metin anahtarı (JWT) verir. Kullanıcı sonraki adımlarda (Örn: Sepete ürün ekleme) şifresini göndermek yerine bu JWT token'ı isteğin başlığına (Header) ekleyerek kendini doğrular.
Kurulum:
pip install passlib[bcrypt] pyjwt
Güvenlik Modülü (security.py):
frompasslib.contextimportCryptContextimportjwtfromdatetimeimportdatetime,timedeltaSECRET_KEY="SUPER_GIZLI_ANAHTAR_KIMSENIN_BILMEMESI_GEREKIR"ALGORITMA="HS256"pwd_context= CryptContext(schemes=["bcrypt"], deprecated="auto")# 1. Şifreyi Hash'lemedefsifre_hashle(plain_password: str) -> str:returnpwd_context.hash(plain_password)# 2. Girilen Şifre Doğru mu Kontrolüdefsifre_dogrula(plain_password: str,hashed_password: str) -> bool:returnpwd_context.verify(plain_password, hashed_password)# 3. JWT Token Üretmedefjwt_token_uret(veri: dict,gecerlilik_suresi_dk: int =15):to_encode= veri.copy()expire= datetime.utcnow() + timedelta(minutes=gecerlilik_suresi_dk)to_encode.update({"exp": expire})encoded_jwt= jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITMA)returnencoded_jwt
Açıklama: Bu bölüm, önceki tüm bölümlerin (FastAPI, Pydantic, SQLAlchemy, Bcrypt ve JWT) birleştiği, uçtan uca üye olma (Register), giriş yapma (Login) ve sadece giriş yapmış kişilerin görebileceği Korumalı Rotaları içeren bitirme projesidir.
Tüm mimariyi birleştiren production-ready güvenli üyelik akışı kod yapısı:
fromfastapiimportFastAPI,Depends,HTTPException,statusfromfastapi.securityimportOAuth2PasswordBearer,OAuth2PasswordRequestFormfromsqlalchemy.ormimportSessionimportdatabaseimportsecurityimportjwtapp= FastAPI(title="Secure Auth Capstone API")# OAuth2 Şeması kurulumu (Swagger dokümantasyonunda kilit ikonu açar)oauth2_scheme= OAuth2PasswordBearer(tokenUrl="login")# SQLAlchemy Kullanıcı Tablo TanımıclassUser(database.Base):__tablename__="users"id= database.Column(database.Integer, primary_key=True, index=True)username= database.Column(database.String, unique=True, index=True)hashed_password= database.Column(database.String)database.Base.metadata.create_all(bind=database.engine)# Pydantic ŞemalarıfrompydanticimportBaseModelclassUserRegister(BaseModel):username: strpassword: str# 1. REGISTER ENDPOINT (Kayıt Olma)@app.post("/register", status_code=status.HTTP_201_CREATED)defregister(user_data: UserRegister,db: Session = Depends(database.get_db)):db_user= db.query(User).filter(User.username == user_data.username).first()ifdb_user:raiseHTTPException(status_code=400, detail="Bu kullanıcı adı zaten alınmış.")# Şifreyi güvenlik modülümüz ile hashleyip kaydediyoruzgizlenmis_sifre= security.sifre_hashle(user_data.password)yeni_user= User(username=user_data.username, hashed_password=gizlenmis_sifre)db.add(yeni_user)db.commit()return{"mesaj":"Kullanıcı başarıyla oluşturuldu."}# 2. LOGIN ENDPOINT (Giriş Yap ve Token Al)@app.post("/login")deflogin(form_data: OAuth2PasswordRequestForm = Depends(),db: Session = Depends(database.get_db)):db_user= db.query(User).filter(User.username == form_data.username).first()ifnotdb_userornotsecurity.sifre_dogrula(form_data.password, db_user.hashed_password):raiseHTTPException(status_code=401, detail="Kullanıcı adı veya şifre hatalı.")# Giriş başarılı ise JWT token üretiliyoraccess_token= security.jwt_token_uret(veri={"sub": db_user.username})return{"access_token": access_token,"token_type":"bearer"}# 3. PROTECTED ROUTE (Sadece Giriş Yapmış Kullanıcılara Özel Bölüm)@app.get("/dashboard/profile")defprofile_sayfasi(token: str = Depends(oauth2_scheme)):try:# Gelen token'ı çözüp doğruluyoruzpayload= jwt.decode(token, security.SECRET_KEY, algorithms=[security.ALGORITMA])username: str = payload.get("sub")ifusernameisNone:raiseHTTPException(status_code=401, detail="Geçersiz Token")exceptjwt.PyJWTError:raiseHTTPException(status_code=401, detail="Token süresi dolmuş veya geçersiz kimlik.")return{"mesaj":f"Hoş geldin{username}, bu bilgi sadece yetkili kullanıcılara gösterilir.","gizli_veri":"Şirket finansal raporu v1.2"}
Açıklama: Yazılan API'lerin yerel bilgisayarımız dışında (localhost), tüm dünyanın erişebileceği bulut sunucularına aktarılması sürecidir. Production ortamında FastAPI uygulamasını doğrudan uvicorn ile değil, işçi süreçlerini (workers) yöneten Gunicorn arkasında Uvicorn işçileriyle çalıştırmak kararlılık sağlar.
Gereksinim
Dosyası (requirements.txt):
Projenin bağımlılıkları listelenir. (pip
freeze > requirements.txt)
Uygulama Sunucusu Hazırlığı
(Procfile
veya Dockerfile): Sunucunun projeyi nasıl başlatacağını
söyleyen komut satırıdır.
web: uvicorn main:app --host 0.0.0.0 --port $PORTPlatform Seçimi: AWS, Railway veya Render paneline GitHub reposu bağlanarak otomatik deploy (CI/CD) tetiklenir.
Gönderdiğiniz eğitim müfredatı belgesini satır satır inceleyerek, istediğiniz kurallar doğrultusunda (süreleri, "Önizleme" ve "ders" ibarelerini, anlamsız satırları temizleyerek) kapsamlı bir geliştirme ve öğrenim rehberine dönüştürdüm.
Bu eğitim, FastAPI ile Test Güdümlü Geliştirme (TDD) felsefesini kullanarak kurumsal, güvenli ve ölçeklenebilir backend projeleri üretmeyi hedefler. İçerik oldukça derin ve uzun olduğu için ilk aşamada Müfredat Tasarımı ve Planlaması ile birlikte "Öğrenecekleriniz" bölümünün detaylı teorik/pratik açılımını yapacağız. Ardından adım adım uygulamalı konu anlatımlarına geçeceğiz.
Eğitimin bu giriş kısmı, modern bir Python backend geliştiricisinin production (canlı ortam) kalitesinde bir mimari kurabilmesi için ihtiyaç duyduğu araç setini tanımlar. Maddelerin teknik karşılıkları ve projeye katkıları şunlardır:
FastAPI, Docker ve Temel Kütüphanelerin Kurulumu: Sadece kodu yazıp lokalde çalıştırmak yerine, projenin her işletim sisteminde aynı şekilde çalışmasını sağlayan Docker konteynerizasyon yapısı kurulur. FastAPI'nin sunduğu asenkron performans kütüphanelerle desteklenir.
Proje
Yapısı ve Bağımlılık Yönetimi: Karmaşık projelerde
kodun kaybolmaması için endüstri standardı olan katmanlı mimari
(Routers, Schemas, Models, Services) inşa edilir. Bağımlılıkların
(pip)
sürümleri kilitlenerek "benim bilgisayarımda çalışıyordu,
sunucuda çalışmıyor" problemi kökten çözülür.
Linting ve
Formatlama Araçları: Kod kalitesini otomatize etmek için
Flake8,
Black
veya Ruff
gibi araçlar entegre edilir. Bu sayede temiz, okunabilir ve PEP 8
standartlarına uygun kod yazılması garantilenir.
Docker ile
PostgreSQL ve Ortam Değişkenleri: Veritabanı bilgisayara
doğrudan kurulmaz, Docker üzerinde izole bir şekilde ayağa
kaldırılır. Şifreler ve gizli anahtarlar kodun içine gömülmez;
python-dotenv
ile .env
dosyalarından güvenli bir şekilde okunur.
SQLAlchemy ve Alembic: SQL sorgularını elle yazmak yerine Python nesneleriyle veritabanı yönetimi (ORM) sağlanır. Veritabanındaki tablo değişiklikleri (yeni kolon ekleme vb.) Alembic ile versiyonlanarak güvenli geçişler (migration) yapılır.
Test
Veritabanı İş akışı ve Pytest: Canlı veritabanına zarar
vermemek için testler çalışırken programatik (otomatik) olarak
sıfırdan geçici bir test veritabanı yaratılır, testler
koşturulur ve işi bittiğinde bu veritabanı yok edilir. Pytest
ile bu süreç otomatize edilir.
ERD Analizi, Yapısal ve Birim (Unit) Testleri: Veritabanı şeması tasarlanırken Entity-Relationship şemaları incelenir. Yazılan testler sadece uç noktaları (endpoint) değil; tabloların gerçekten oluşup oluşmadığını, kısıtlamaları (constraints), veri uzunluklarını ve ilişkileri de denetler.
Pydantic Şemaları ve Hata Günlüğü (Logging): API'ye giren ve çıkan verilerin doğrulanması (validation) ve dönüştürülmesi (serialization) Pydantic ile yapılır. Sistemde oluşan hatalar print etmek yerine log dosyalarına profesyonelce kaydedilir.
CRUD Operasyonları için Entegrasyon (Integration) Testleri: Unit testlerin ötesine geçilerek; API uç noktasına istek atıldığında veritabanına gerçekten yazılıp yazılmadığı, status kodların (201, 404, 200) doğruluğu uçtan uca test edilir.
Kod Kapsama (Code Coverage) ve Manuel Doğrulama: Yazılan testlerin projenin yüzde kaçını koruduğu (coverage) ölçülür (Hedef genellikle >%80'dir). Son olarak Swagger UI (FastAPI Docs) üzerinden manuel denemeler gerçekleştirilir.
Projemizi inşa ederken uygulayacağımız klasör yapısı ve veri akış şeması şu şekilde tasarlanmıştır:
fastapi_tdd_project/│├── .env # Gizli ortam değişkenleri├── Dockerfile # API imaj tanımı├── docker-compose.yml # PostgreSQL, Adminer ve Test DB servisleri├── requirements.txt # Bağımlılıklar├── alembic.ini # Veritabanı göç konfigürasyonu│├── app/│ ├── __init__.py│ ├── main.py # Uygulama ana giriş kapısı│ ├── config.py # Ayarların pydantic ile okunması│ ├── database.py # SQLAlchemy bağlantı motoru│ ├── models/ # Veritabanı tabloları (SQLAlchemy)│ ├── schemas/ # Veri doğrulama kalıpları (Pydantic)│ └── routers/ # API Uç noktaları (Endpoints)│└── tests/├── __init__.py├── conftest.py # Pytest fixture'ları (DB sıfırlama otomasyonu)├── test_structure.py # Tablo ve şema doğrulama testleri└── test_endpoints.py # CRUD API entegrasyon testleri
Python 3.x Kurulumları ve VS Code Konfigürasyonu
Sanal Ortamların (Virtual Environments) Mantığı ve Kurulumu
FastAPI
Projesinin Başlatılması ve Bağımlılık Yönetimi (pip)
Proje Linting (flake8)
ve Formatlama (black)
Ayarları
Sanal ortamlar, bilgisayarınızdaki
küresel Python kurulumunu kirletmeden her projeye özel izole
kütüphane odaları yaratmanızı sağlar. pip
ile bağımlılık yönetimi yaparken kütüphane sürümlerini
sabitlemek production ortamında çökme yaşanmasını engeller.
Black
kodu otomatik olarak standartlara göre biçimlendirirken, Flake8
kod hatalarını ve standart dışı yazımları tarar.
Proje klasörünü oluşturalım, sanal ortamı aktif edip temel paketleri yükleyelim.
# Klasör oluşturma ve girişmkdir fastapi_tdd_project && cd fastapi_tdd_project# Sanal ortam oluşturma (Windows/Mac ortak mantığı)python -m venv venv# Sanal ortamı aktif etme# Windows için: venv\Scripts\activate# Mac/Linux için:source venv/activate# Gerekli bağımlılıkların requirements.txt dosyasına yazılmasıcat<<EOT >> requirements.txtfastapi==0.110.0uvicorn==0.28.0pydantic==2.6.4black==24.2.0flake8==7.0.0EOT# Paketlerin yüklenmesipip install -r requirements.txt
app/main.py
dosyasının ilk hali:
fromfastapiimportFastAPIapp= FastAPI(title="FastAPI TDD E-Ticaret API")@app.get("/")defread_root():return{"status":"Uygulama Çalışıyor"}
Docker Nedir? Neden Kullanılır?
PostgreSQL Veritabanının Dockerize Edilmesi
python-dotenv
ile Güvenli Veri Yönetimi
SQLAlchemy Bağlantı Yönetimi
Alembic ile Veritabanı Migrasyonları
Docker, veritabanı gibi dış
bileşenleri bilgisayarınıza doğrudan kurup konfigürasyon
karmaşası yaşamak yerine, hafif bir sanal konteyner içinde
saniyeler içinde ayağa kaldırmanızı sağlar. SQLAlchemy,
Python sınıflarını (Class) SQL tablolarına dönüştüren bir
ORM mimarisidir. Veritabanında bir kolon eklediğimizde bunu canlıya
yansıtmak için Alembic
aracıyla sürüm kontrolü (git benzeri) sağlarız.
Docker ortamını ve veritabanı bağlantı ayarlarını yapalım.
.env
Dosyası:
DATABASE_URL=postgresql://user_tdd:pass_tdd@localhost:5432/db_tdd
docker-compose.yml
Dosyası:
version:'3.8'services:db:image:postgres:15-alpinecontainer_name: tdd_postgres_dbenvironment:POSTGRES_USER: user_tddPOSTGRES_PASSWORD: pass_tddPOSTGRES_DB: db_tddports:-"5432:5432"volumes:-postgres_data:/var/lib/postgresql/dataadminer:image: adminercontainer_name: tdd_adminerports:-"8080:8080"volumes:postgres_data:
(Bu dosyayı hazırladıktan
sonra terminalde docker-compose
up -d
komutu çalıştırılarak PostgreSQL ve arayüz aracı Adminer ayağa
kaldırılır).
app/database.py
Dosyası:
importosfromdotenvimportload_filefromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportdeclarative_base,sessionmaker# .env dosyasını yükleload_file()DATABASE_URL= os.getenv("DATABASE_URL")engine= create_engine(DATABASE_URL)SessionLocal= sessionmaker(autocommit=False, autoflush=False, bind=engine)Base= declarative_base()# Veritabanı oturum bağımlılığıdefget_db():db= SessionLocal()try:yielddbfinally:db.close()
Test Veritabanı İş Akışı (Workflow)
Pytest Kurulumu ve Yapılandırılması
Programatik Olarak Docker Test Veritabanı Oluşturma ve Silme
TDD (Test-Driven Development) yaparken en kritik kural testlerin izole olmasıdır. Testler gerçek veritabanındaki verileri bozmamalı, değiştirmemelidir. Bu yüzden testler başlarken geçici bir veritabanı şeması açılır, test verileriyle CRUD işlemleri yapılır ve test bittiğinde bu geçici alan tamamen yok edilir.
pytest
paketini requirements'a ekleyip testlerin merkezi yönetim kalbi olan
conftest.py
yapısını kuralım.
tests/conftest.py
Dosyası:
importpytestfromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportsessionmakerfromapp.databaseimportBasefromapp.mainimportappfromfastapi.testclientimportTestClient# Test için izole geçici veritabanı (Bellekte veya test DB'sinde)TEST_DATABASE_URL="sqlite:///./test_temp.db"# Kolaylık adına sqlite örneklenmiştirengine= create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread":False})TestingSessionLocal= sessionmaker(autocommit=False, autoflush=False, bind=engine)@pytest.fixture(scope="session", autouse=True)defsetup_database():# Test başlamadan önce tabloları sıfırdan oluşturBase.metadata.create_all(bind=engine)yield# Testler bittikten sonra tabloları temizle / silBase.metadata.drop_all(bind=engine)@pytest.fixturedefdb_session():connection= engine.connect()transaction= connection.begin()session= TestingSessionLocal(bind=connection)yieldsessionsession.close()transaction.rollback()connection.close()@pytest.fixturedefclient(db_session):# Uygulamanın veritabanı bağımlılığını test session'ı ile değiştiriyoruzdefoverride_get_db():try:yielddb_sessionfinally:passapp.dependency_overrides[app.database.get_db] = override_get_dbyieldTestClient(app)delapp.dependency_overrides[app.database.get_db]
Proje ERD (Entity-Relationship Diagram) Analizi
Kategori (Category) ve Ürün (Product) Tablo Gereksinimleri
Tablo Varlık, Kolon, Kısıtlama (Nullable, Unique, Length) Testlerinin Yazılması
Modellerin Hayata Geçirilmesi
Geleneksel geliştirmede önce tablo oluşturulur, sonra kod yazılır. TDD yaklaşımında ise önce tablonun yapısal özelliklerini denetleyen test yazılır (Test Kalır - Red), ardından tablo modeli oluşturulur (Test Geçer - Green). Bu sayede veritabanı şemasında planlanan hiçbir kısıtlama (örneğin ismin boş geçilemez olması veya benzersiz olması) gözden kaçmaz.
Kategori tablomuzda id
(Primary Key), name
(String, Benzersiz, Boş bırakılamaz) ve is_active
(Boolean, Varsayılan: True) alanları olmasını istiyoruz.
tests/test_structure.py
Dosyası:
fromsqlalchemyimportinspectfromapp.databaseimportenginefromapp.models.categoryimportCategorydeftest_category_table_structure():inspector= inspect(engine)columns= inspector.get_columns("category")# 1. Test Case: Tablo var mı ve kolon isimleri doğru mu?col_names= [c["name"]forcincolumns]assert"id"incol_namesassert"name"incol_namesassert"is_active"incol_names# 2. Test Case: Nullable (Boş bırakılabilirlik) kontrolüforcincolumns:ifc["name"] =="name":assertc["nullable"]isFalse# name alanı boş bırakılamaz olmalıdeftest_category_model_attributes():# 3. Test Case: Varsayılan değer kontrolücat= Category(name="Elektronik")assertcat.is_activeisTrue
(Bu testi şimdi
çalıştırırsak Category
modeli henüz var olmadığı için hata verecektir).
Şimdi Modeli Oluşturup Testi Geçirelim
(app/models/category.py):
fromsqlalchemyimportBoolean,Column,Integer,Stringfromapp.databaseimportBaseclassCategory(Base):__tablename__="category"id= Column(Integer, primary_key=True, index=True)name= Column(String, unique=True, nullable=False)is_active= Column(Boolean, default=True)
FastAPI Error Logging
Pydantic Şemaları ile Serileştirme ve Doğrulama
Post, Get, Put, Delete Metotları İçin Unit Test Geliştirme
Router Yapısı ve Hata Yönetimi Exception Handling
Pydantic şemaları API'ye gelen
JSON verisini doğrular. Eğer istemci eksik veya hatalı veri tipi
gönderirse, FastAPI otomatik olarak 422
Unprocessable Entity
döner. Unit test aşamasında mock yapılar veya doğrudan router
fonksiyonları çağrılarak uç noktaların davranış mantığı
(İstisnai durumlar dahil) denetlenir.
Önce Pydantic Şemasını Tasarlayalım
(app/schemas/category.py):
frompydanticimportBaseModel,FieldclassCategoryCreate(BaseModel):name: str = Field(..., min_length=2, max_length=50)classCategoryResponse(BaseModel):id: intname: stris_active: boolclassConfig:from_attributes=True
Endpoint Testlerinin Yazılması (tests/test_endpoints.py):
deftest_create_category_endpoint(client):# Başarılı kayıt senaryosuresponse= client.post("/categories/", json={"name":"Ev & Yaşam"})assertresponse.status_code ==201assertresponse.json()["name"] =="Ev & Yaşam"assert"id"inresponse.json()deftest_create_category_bad_request(client):# Geçersiz veri (Kısa isim) gönderme senaryosuresponse= client.post("/categories/", json={"name":"A"})assertresponse.status_code ==422# Pydantic validation hatası
Router ve Endpoint Kodunun Yazılması (app/routers/category.py):
fromfastapiimportAPIRouter,Depends,HTTPException,statusfromsqlalchemy.ormimportSessionfromapp.databaseimportget_dbfromapp.models.categoryimportCategoryfromapp.schemas.categoryimportCategoryCreate,CategoryResponserouter= APIRouter(prefix="/categories", tags=["Categories"])@router.post("/", response_model=CategoryResponse, status_code=status.HTTP_201_CREATED)defcreate_category(category_in: CategoryCreate,db: Session = Depends(get_db)):# Benzersizlik kontrolüdb_cat= db.query(Category).filter(Category.name == category_in.name).first()ifdb_cat:raiseHTTPException(status_code=400, detail="Bu kategori zaten mevcut.")new_category= Category(name=category_in.name)db.add(new_category)db.commit()db.refresh(new_category)returnnew_category
Entegrasyon Testi Nedir? Test Planı Oluşturma
CRUD Operasyonlarının Uçtan Uca Doğrulanması
Kod Kapsama Analizi (Code Coverage)
FastAPI Docs ile Manuel Test
Entegrasyon testleri, sistemin tüm
parçalarının (API katmanı, Pydantic doğrulama katmanı, SQL
Veritabanı ve kısıtlamalar) bir bütün olarak kusursuz entegre
olup olmadığını doğrular. Kod kapsama analizi (pytest-cov),
yazdığımız testlerin tüm proje satırlarının ne kadarına
dokunduğunu söyler.
Kategorileri Listeleme (Get All), Güncelleme (Put) ve Silme (Delete) süreçlerinin entegrasyon testlerini ekleyelim.
tests/test_endpoints.py
Devamı (Integration Tests):
deftest_get_all_categories_integration(client,db_session):# Veritabanına test öncesi elle iki kayıt atalımcat1= Category(name="Moda")cat2= Category(name="Kozmetik")db_session.add_all([cat1, cat2])db_session.commit()# API'den tüm listeyi isteyelimresponse= client.get("/categories/")assertresponse.status_code ==200assertlen(response.json()) >=2deftest_delete_category_not_found(client):# Olmayan bir kategoriyi silmeye çalışma senaryosuresponse= client.delete("/categories/9999")assertresponse.status_code ==404
app/routers/category.py
Tamamlayıcı CRUD Kodları:
fromtypingimportList@router.get("/", response_model=List[CategoryResponse])defget_all_categories(db: Session = Depends(get_db)):returndb.query(Category).all()@router.delete("/{category_id}", status_code=status.HTTP_204_NO_CONTENT)defdelete_category(category_id: int,db: Session = Depends(get_db)):db_cat= db.query(Category).filter(Category.id == category_id).first()ifnotdb_cat:raiseHTTPException(status_code=404, detail="Silinmek istenen kategori bulunamadı.")db.delete(db_cat)db.commit()returnNone
Terminal üzerinden aşağıdaki komutlar çalıştırılarak test süreçleri izlenir:
# Tüm testleri çalıştırırpytest# Kod kapsama oranını (Coverage) ekrana basarpytest --cov=app tests/
Eğitimin ikinci büyük aşaması olan Alembic Çoklu Veritabanı (Multi-Database) Yapılandırması ile İleri Düzey Tablo İlişkileri (ERD) ve Kısıtlama Testleri konularını sırasıyla, en ince yapısal detaylarına kadar ele alıyoruz.
Büyük ölçekli projelerde uygulama veritabanı (production) ile test veritabanının (test) veya operasyonel veritabanı (OLTP) ile analitik veritabanının (OLAP) şemalarını aynı anda göç ettirmek (migration) gerekebilir. Alembic buna "Multidb" veya "Multiple Databases" mimarisi der.
Çoklu veritabanı senaryosunda tek
bir alembic.ini
ve tek bir env.py
dosyası üzerinden birden fazla SQLAlchemy engine
nesnesi yönetilir. Değişiklikler her veritabanı motoru için ayrı
birer "bölüm" (section) olarak kaydedilir.
1. alembic.ini
Dosyasının Yapılandırılması
Alembic
projenizi alembic
init --template multidb alembic_multidb
komutuyla başlattığınızda veya mevcut dosyayı düzenlediğinizde
veritabanı anahtarlarını tanımlamanız gerekir:
[alembic]script_location = alembic_multidb# Yönetilecek veritabanlarının isim listesidatabases = engine_prod, engine_test[engine_prod]sqlalchemy.url = postgresql://user_tdd:pass_tdd@localhost:5432/db_prod[engine_test]sqlalchemy.url = postgresql://user_tdd:pass_tdd@localhost:5432/db_test_clean
2. alembic_multidb/env.py
Dosyasının Düzenlenmesi
Çoklu
şema takibi için env.py
içerisindeki run_migrations_online
fonksiyonu her bir veritabanı motorunu tek tek dönerek
tetiklemelidir:
importosfromlogging.configimportfileConfigfromalembicimportcontextfromsqlalchemyimportengine_from_config,poolfromapp.databaseimportBase# Modellerinizin bağlı olduğu Base sınıfıconfig= context.configifconfig.config_file_nameisnotNone:fileConfig(config.config_file_name)# Hedef metadata nesnesitarget_metadata= {"engine_prod": Base.metadata,"engine_test": Base.metadata,}defrun_migrations_online():"""Çoklu veritabanı motoru için online migrasyon koşturma senaryosu."""# alembic.ini içindeki 'databases' değerini okudb_names= config.get_main_option("databases","").split(",")engines= {}fornameindb_names:name= name.strip()# Her veritabanı için dinamik url'leri çevresel değişkenlerden de ezebilirsinizengines[name] = engine_from_config(config.get_section(name),prefix="sqlalchemy.",poolclass=pool.NullPool,)forname,engineinengines.items():print(f"---{name}Veritabanı İçin Göç Senaryoları Çalıştırılıyor ---")connection= engine.connect()context.configure(connection=connection,target_metadata=target_metadata.get(name),)try:withcontext.begin_transaction():context.run_migrations(engine_name=name)finally:connection.close()ifcontext.is_offline_mode():raiseNotImplementedError("Çoklu veritabanı mimarisinde offline mod desteklenmemektedir.")else:run_migrations_online()
E-ticaret sistemimizin çekirdeğini oluşturan gelişmiş ürün varyasyon mimarisine odaklanıyoruz. Tasarlayacağımız ERD (İlişki) şeması şu kısıtlamaları içermektedir:
ProductLine (Ürün Serisi/Varyasyonu): Bir ürünün birden fazla varyasyonu (örn: Kırmızı-M, Mavi-L) olabilir.
ProductImage
(Ürün Görseli): Her ürün varyasyonunun sırasını (order)
belirten birden fazla görseli olur.
Seasonal Events (Mevsimsel Kampanyalar): Varyasyonların veya ürünlerin dahil olduğu dönemlik kısıtlamalar.
Attributes & AttributeValue (Özellikler & Değerler): "Renk" (Attribute) -> "Kırmızı" (AttributeValue).
ProductLine-AttributeValue: Çoka çok (Many-to-Many) ilişki tablosu.
Product-ProductType: Ürün ile ürün tiplerini bağlayan çoka çok (Many-to-Many) ilişki tablosu.
id:
Primary Key.
price:
Decimal (Hassas para birimi verisi, Boş bırakılamaz).
sku:
String, Benzersiz (Unique), Maksimum 20 karakter.
product_id:
Foreign Key (product.id),
ondelete="CASCADE".
order:
Integer, Varyasyonun listelenme sırası.
tests/test_product_line.py)TDD felsefesine sadık kalmak adına, henüz tabloyu yazmadan yapısal gereksinimlerimizi denetleyen testimizi kurguluyoruz:
importpytestfromsqlalchemyimportinspectfromsqlalchemy.excimportIntegrityErrorfromapp.databaseimportenginefromapp.models.product_lineimportProductLinedeftest_product_line_constraints():inspector= inspect(engine)columns= inspector.get_columns("product_line")col_dict= {c["name"]: cforcincolumns}# 1. SKU Uzunluğu ve Nullable Kontrolüassertcol_dict["sku"]["nullable"]isFalse# Not: SQLite ve Postgres tip uzunluklarını farklı saklayabilir, kontrol hassas olmalıdırassertcol_dict["price"]["nullable"]isFalse# 2. Foreign Key Kontrolüfkeys= inspector.get_foreign_keys("product_line")fk_products= [fkforfkinfkeysiffk["referred_table"] =="product"]assertlen(fk_products) ==1assertfk_products[0]["referred_columns"] == ["id"]
app/models/product_line.py)Testi başarıyla geçirecek kaynak kodumuzu yazıyoruz:
fromsqlalchemyimportColumn,Decimal,ForeignKey,Integer,Stringfromsqlalchemy.ormimportrelationshipfromapp.databaseimportBaseclassProductLine(Base):__tablename__="product_line"id= Column(Integer, primary_key=True, index=True)price= Column(Decimal(10,2), nullable=False)sku= Column(String(20), unique=True, nullable=False, index=True)order= Column(Integer, default=1, nullable=False)# İlişkilerproduct_id= Column(Integer, ForeignKey("product.id", ondelete="CASCADE"), nullable=False)product= relationship("Product", back_populates="product_lines")images= relationship("ProductImage", back_populates="product_line", cascade="all, delete-orphan")
url:
String, Görsel adresi, Boş bırakılamaz.
order:
Integer, Bir varyasyon içindeki sıralama düzeni.
product_line_id:
Foreign Key (product_line.id),
Bağımlı varyasyon silindiğinde otomatik silinmeli (CASCADE).
tests/test_product_image.py)deftest_product_image_structure():inspector= inspect(engine)columns= inspector.get_columns("product_image")col_dict= {c["name"]: cforcincolumns}assert"url"incol_dictassertcol_dict["url"]["nullable"]isFalse# İlişki kontrolüfkeys= inspector.get_foreign_keys("product_image")assertany(fk["referred_table"] =="product_line"forfkinfkeys)
app/models/product_image.py)fromsqlalchemyimportColumn,ForeignKey,Integer,Stringfromsqlalchemy.ormimportrelationshipfromapp.databaseimportBaseclassProductImage(Base):__tablename__="product_image"id= Column(Integer, primary_key=True, index=True)url= Column(String(255), nullable=False)order= Column(Integer, nullable=False, default=1)product_line_id= Column(Integer,ForeignKey("product_line.id", ondelete="CASCADE"),nullable=False,)product_line= relationship("ProductLine", back_populates="images")
name:
Kampanya adı (Örn: "Kara Cuma 2026"), Benzersiz, Boş
bırakılamaz.
start_date
& end_date:
Kampanyanın geçerlilik süreleri.
tests/test_seasonal_events.py)deftest_seasonal_event_unique_constraint(db_session):fromapp.models.seasonal_eventimportSeasonalEventimportdatetimeevent1= SeasonalEvent(name="Kış İndirimi",start_date=datetime.date.today(),end_date=datetime.date.today(),)db_session.add(event1)db_session.commit()# Aynı isimle ikinci kayıt eklenmeye çalışıldığında benzersizlik hatası fırlatmalıevent2= SeasonalEvent(name="Kış İndirimi",start_date=datetime.date.today(),end_date=datetime.date.today(),)db_session.add(event2)withpytest.raises(IntegrityError):db_session.commit()
app/models/seasonal_event.py)fromsqlalchemyimportColumn,Date,Integer,Stringfromapp.databaseimportBaseclassSeasonalEvent(Base):__tablename__="seasonal_event"id= Column(Integer, primary_key=True, index=True)name= Column(String(100), unique=True, nullable=False)start_date= Column(Date, nullable=False)end_date= Column(Date, nullable=False)
Dinamik bir ürün filtreleme
sistemi kurmak için Attribute
(Özellik Tipi) ve AttributeValue
(Özellik Değeri) arasında 1-to-Many ilişkisi tasarlanır.
tests/test_attributes.py)deftest_attribute_and_value_relation(db_session):fromapp.models.attributeimportAttributefromapp.models.attribute_valueimportAttributeValue# Renk adında bir özellik tipi ekleyelimcolor_attr= Attribute(name="Renk", description="Ürün renk varyasyonları")db_session.add(color_attr)db_session.commit()# Bu tipe bağlı değerler ekleyelimval1= AttributeValue(value="Kırmızı", attribute_id=color_attr.id)val2= AttributeValue(value="Mavi", attribute_id=color_attr.id)db_session.add_all([val1, val2])db_session.commit()assertlen(color_attr.values) ==2assertcolor_attr.values[0].value =="Kırmızı"
app/models/attribute.py
Dosyası:
fromsqlalchemyimportColumn,Integer,Stringfromsqlalchemy.ormimportrelationshipfromapp.databaseimportBaseclassAttribute(Base):__tablename__="attribute"id= Column(Integer, primary_key=True, index=True)name= Column(String(50), unique=True, nullable=False)description= Column(String(255), nullable=True)values= relationship("AttributeValue", back_populates="attribute")
app/models/attribute_value.py
Dosyası:
fromsqlalchemyimportColumn,ForeignKey,Integer,Stringfromsqlalchemy.ormimportrelationshipfromapp.databaseimportBaseclassAttributeValue(Base):__tablename__="attribute_value"id= Column(Integer, primary_key=True, index=True)value= Column(String(50), nullable=False)attribute_id= Column(Integer, ForeignKey("attribute.id", ondelete="CASCADE"), nullable=False)attribute= relationship("Attribute", back_populates="values")
Müfredatımızın en kritik yapısal test aşamalarından biri olan ara (association) tabloların kurulumudur.
ProductLine
- AttributeValue Table: Hangi varyasyonun hangi özellik
değerlerine sahip olduğunu eşler (Örn: SKU-RED-XL
hem Kırmızı hem XL değerine bağlanır).
Product - ProductType Table: Ürünlerin hangi kategorik tiplere girdiğini eşler.
1. ProductLine-AttributeValue Many-to-Many Tanımı
(app/models/product_line_attribute_value.py)
fromsqlalchemyimportColumn,ForeignKey,Integer,Tablefromapp.databaseimportBase# Saf ilişki tablosu (Association Table)product_line_attribute_value_association= Table("product_line_attribute_value",Base.metadata,Column("product_line_id",Integer,ForeignKey("product_line.id", ondelete="CASCADE"),primary_key=True,),Column("attribute_value_id",Integer,ForeignKey("attribute_value.id", ondelete="CASCADE"),primary_key=True,),)
2. Product-ProductType Many-to-Many Tanımı
(app/models/product_product_type.py)
fromsqlalchemyimportColumn,ForeignKey,Integer,Tablefromapp.databaseimportBaseproduct_product_type_association= Table("product_product_type",Base.metadata,Column("product_id",Integer,ForeignKey("product.id", ondelete="CASCADE"),primary_key=True,),Column("product_type_id",Integer,ForeignKey("product_type.id", ondelete="CASCADE"),primary_key=True,),)
(Bu ara tablolar
oluşturulduktan sonra ana koda dönülerek ProductLine
modeline attribute_values
= relationship("AttributeValue",
secondary=product_line_attribute_value_association)
bağı eklenir.)
tests/test_many_to_many.py)Sistemde çoka çok verilerin silme senaryolarında (Cascade) öksüz kalıp kalmadığını denetleyen kapsamlı testimiz:
deftest_product_line_attribute_value_flow(db_session):fromapp.models.product_lineimportProductLinefromapp.models.attribute_valueimportAttributeValuefromapp.models.attributeimportAttribute# Altyapı Hazırlığıattr= Attribute(name="Beden")db_session.add(attr)db_session.commit()val= AttributeValue(value="Large", attribute_id=attr.id)db_session.add(val)db_session.commit()# Ara tabloya kayıt atma simülasyonu# Bir product_line nesnesine doğrudan .attribute_values.append() yöntemiyle ekleme yapılabilir.
İleri düzey veri modellerimiz ve aralarındaki çoka çok (Many-to-Many) ilişkiler tamamlandığına göre, şimdi bu yapıyı istemciye güvenli bir şekilde sunma katmanına geçiyoruz.
Önce karmaşık ilişkisel verileri iç içe geçmiş (nested) JSON formatına hatasız dönüştüren Pydantic Serileştirme (Serialization) ve Doğrulama (Validation) Şemalarını yazacak ve test edeceğiz. Ardından, tüm bu ilişkileri tek bir istekte birleştiren Uç Nokta (Endpoint) Entegrasyon Testlerini hayata geçireceğiz.
SQLAlchemy veritabanından gelen
veriler Python nesne grafikleridir (ilişkili nesneler zinciri).
Pydantic v2 ile gelen from_attributes=True
ayarı, bu nesneleri otomatik olarak okur ve doğrulanmış temiz
JSON çıktılarına serileştirir. Test aşamasında, veritabanından
veri çekiyormuş gibi sahte (mock) nesneler üreterek şemaların
doğruluğunu denetleriz.
app/schemas/product_composite.py)Tüm gelişmiş şemaları, iç içe geçecek şekilde tek bir yapıda tasarlayalım:
fromtypingimportList,OptionalfrompydanticimportBaseModel,Field,ConfigDictfromdecimalimportDecimal# --- Özellik Değeri Şeması ---classAttributeValueResponse(BaseModel):model_config= ConfigDict(from_attributes=True)id: intvalue: str# --- Ürün Görseli Şeması ---classProductImageResponse(BaseModel):model_config= ConfigDict(from_attributes=True)id: inturl: strorder: int# --- Ürün Varyasyon (ProductLine) Şeması ---classProductLineResponse(BaseModel):model_config= ConfigDict(from_attributes=True)id: intprice: Decimalsku: strorder: intimages: List[ProductImageResponse]attribute_values: List[AttributeValueResponse] = Field(..., alias="attribute_values")# --- Ana Ürün Detay Şeması (İç içe geçmiş tüm ilişkiler dahil) ---classProductDetailResponse(BaseModel):model_config= ConfigDict(from_attributes=True)id: intname: strdescription: Optional[str] =Noneproduct_lines: List[ProductLineResponse]
tests/test_pydantic_serialization.py)Şemalarımızın ilişkisel verileri kayıpsız ve doğru tiplerle JSON verisine dönüştürdüğünü doğrulamak için TDD testini kurguluyoruz:
importpytestfromdecimalimportDecimalfromapp.schemas.product_compositeimportProductDetailResponsedeftest_product_detail_serialization_and_aliases():# 1. Sahte (Mock) ilişkisel nesne yapısı kurgulayalımclassMockAttributeValue:def__init__(self,id,value):self.id = idself.value = valueclassMockProductImage:def__init__(self,id,url,order):self.id = idself.url = urlself.order = orderclassMockProductLine:def__init__(self,id,price,sku,order,images,attribute_values):self.id = idself.price = priceself.sku = skuself.order = orderself.images = imagesself.attribute_values = attribute_valuesclassMockProduct:def__init__(self,id,name,description,product_lines):self.id = idself.name = nameself.description = descriptionself.product_lines = product_lines# 2. İlişki zincirini bağlayalım (Örn: Kırmızı renkli, görseli olan bir XL tişört varyasyonu)mock_attr= MockAttributeValue(id=10, value="Kırmızı")mock_img= MockProductImage(id=100, url="https://example.com", order=1)mock_line= MockProductLine(id=50,price=Decimal("299.99"),sku="TSHIRT-RED-XL",order=1,images=[mock_img],attribute_values=[mock_attr])mock_product= MockProduct(id=1,name="Oversize Tişört",description="Pamuklu rahat kesim",product_lines=[mock_line])# 3. Serileştirme İşlemini Tetikleyelimserialized_data= ProductDetailResponse.model_validate(mock_product).model_dump()# 4. Doğrulama (Assertions)assertserialized_data["id"] ==1assertserialized_data["name"] =="Oversize Tişört"assertlen(serialized_data["product_lines"]) ==1# İç içe geçmiş alanların kontrolütarget_line= serialized_data["product_lines"][0]asserttarget_line["sku"] =="TSHIRT-RED-XL"asserttarget_line["price"] == Decimal("299.99")asserttarget_line["images"][0]["url"] =="https://example.com"asserttarget_line["attribute_values"][0]["value"] =="Kırmızı"
Bu aşamada, veritabanına gerçek ilişkisel verileri yazacak, FastAPI istemcisi üzerinden tek bir istek atacak ve veritabanı motorunun join işlemlerini doğru yapıp yapmadığını uçtan uca denetleyeceğiz.
tests/test_endpoints_integration.py)
Bu test, istemcinin
/products/{product_id}
endpoint'ine istek attığında dönen cevabın veritabanındaki tüm
ilişkileri (Görseller, Varyasyonlar, Özellikler) doğru getirip
getirmediğini doğrular:
importpytestfromapp.models.categoryimportCategory# Ürün ana modelimizin mevcut olduğunu varsayıyoruzfromsqlalchemyimportColumn,Integer,String,ForeignKeyfromapp.databaseimportBase# Test hazırlığı için mock veya gerçek Product tablosu bağlantısıclassProduct(Base):__tablename__="product"id= Column(Integer, primary_key=True, index=True)name= Column(String, nullable=False)description= Column(String)deftest_get_product_detail_with_all_relations_integration(client,db_session):fromapp.models.product_lineimportProductLinefromapp.models.product_imageimportProductImagefromapp.models.attributeimportAttributefromapp.models.attribute_valueimportAttributeValue# 1. ADIM: Veritabanına Gerçek İlişkisel İlişkileri Yazalım (Arrange)# Ana Ürünprod= Product(name="Koşu Ayakkabısı", description="Performans odaklı")db_session.add(prod)db_session.commit()# Özellik ve Özellik Değeri (Many-to-Many için)attr= Attribute(name="Numara")db_session.add(attr)db_session.commit()val= AttributeValue(value="42", attribute_id=attr.id)db_session.add(val)db_session.commit()# Ürün Varyasyonu (ProductLine)line= ProductLine(price=1499.50, sku="SHOES-42-RED", order=1, product_id=prod.id)# Çoka çok ilişki bağlamaline.attribute_values.append(val)db_session.add(line)db_session.commit()# Varyasyon Görseli (ProductImage)img= ProductImage(url="https://example.com", order=1, product_line_id=line.id)db_session.add(img)db_session.commit()# 2. ADIM: API Uç Noktasını Tetikleyelim (Act)response= client.get(f"/products/{prod.id}")# 3. ADIM: Sonuçları Uçtan Uca Doğrulayalım (Assert)assertresponse.status_code ==200res_data= response.json()assertres_data["name"] =="Koşu Ayakkabısı"assertlen(res_data["product_lines"]) ==1# Varyasyon içerik kontrolüassertres_data["product_lines"][0]["sku"] =="SHOES-42-RED"# Decimal verinin str/float dönüşüm kontrolüassertfloat(res_data["product_lines"][0]["price"]) ==1499.50# Görsel ve Many-to-Many ilişki kontrolüassertres_data["product_lines"][0]["images"][0]["url"] =="https://example.com"assertres_data["product_lines"][0]["attribute_values"][0]["value"] =="42"deftest_get_product_detail_not_found(client):# Olmayan ürün senaryosuresponse= client.get("/products/99999")assertresponse.status_code ==404assertresponse.json()["detail"] =="Aranan ürün sistemde bulunamadı."
app/routers/product.py)
Yukarıdaki zorlu entegrasyon
testlerini başarıyla yeşile (Green)
çevirecek olan optimize edilmiş veritabanı sorgusunu (Eager
Loading kullanarak) yazıyoruz:
fromfastapiimportAPIRouter,Depends,HTTPException,statusfromsqlalchemy.ormimportSession,joinedloadfromapp.databaseimportget_db# Testteki Product modelinin import edildiğini varsayıyoruzfromtests.test_endpoints_integrationimportProductfromapp.models.product_lineimportProductLinefromapp.schemas.product_compositeimportProductDetailResponserouter= APIRouter(prefix="/products", tags=["Products"])@router.get("/{product_id}", response_model=ProductDetailResponse)defget_product_detail(product_id: int,db: Session = Depends(get_db)):# N+1 sorgu problemini engellemek için joinedload (Eager Loading) kullanıyoruzproduct= db.query(Product).options(joinedload(Product.product_lines).joinedload(ProductLine.images),joinedload(Product.product_lines).joinedload(ProductLine.attribute_values)).filter(Product.id == product_id).first()ifnotproduct:raiseHTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Aranan ürün sistemde bulunamadı.")returnproduct
Tüm geliştirdiğimiz bu router
mekanizmalarını app/main.py
dosyasına dahil ederek projeyi ayağa kaldırmaya hazır hale
getiriyoruz.
app/main.py
Son Hali:
fromfastapiimportFastAPIfromapp.routersimportcategory,productapp= FastAPI(title="FastAPI TDD Kurumsal Varyasyonlu Ürün API",description="İleri düzey ERD ilişkileri ve çoklu DB mimarisine sahip backend sistemi",version="1.0.0")# Router'ların uygulamaya dahil edilmesiapp.include_router(category.router)app.include_router(product.router)@app.get("/", tags=["Root"])defroot_status():return{"status":"Sistem Sağlıklı, Tüm Testler Aktif"}
Aşağıdaki komutu terminalde çalıştırarak yazdığımız karmaşık mimarinin test kapsama (coverage) başarısını görebiliriz:
pytest --cov=app tests/Böylece müfredatınızda yer alan tüm veri şemalarını, test stratejilerini, serileştirme mimarilerini ve entegrasyon süreçlerini uçtan uca pratik ve derinlemesine uygulamalarla tamamlamış olduk.
*******************************************************************************************
Görselde yer alan FastAPI Complete Course: JWT, SQLModel, Celery & Deployment kurs müfredatını sizden gelen tüm kurallar doğrultusunda (süreleri, "Önizleme" ve "ders" ibrelerini, anlamsız satırları temizleyerek) ve hiçbir şekilde özet yapmadan derinlemesine, teorik açıklamalar, pratik kod örnekleri ve uygulamalarla tasarlayarak parça parça ele alıyorum.
Dosyanın büyüklüğü ve konuların derinliği nedeniyle eğitim serisinin 1. Parçasını (Giriş, "Öğrenecekleriniz" bölümü ve Temel Web Sunucusu İşlemleri) aşağıda eksiksiz bir şekilde başlatıyorum.
Bu kursun ana hedefi, modern ve asenkron bir Python web framework'ü olan FastAPI kullanarak, üretim ortamına (production) hazır, güvenli ve ölçeklenebilir RESTful API'lar geliştirmektir. "Öğrenecekleriniz" başlığı altında verilen maddelerin teknik karşılıkları şu şekildedir:
Modüler
Proje Yapısı ve Scalable Tasarım: Küçük projelerde tüm
kodlar tek bir dosyada (main.py)
yazılabilir. Ancak proje büyüdükçe bu yapı sürdürülemez
hale gelir. Kurs, projeyi mantıksal katmanlara (Routers, Services,
Models, Schemas) bölerek büyük ekiplerin eş zamanlı
çalışabileceği modüler bir mimariyi öğretir.
Gelişmiş Veri Doğrulama ve CRUD İşlemleri (SQLModel): Gelen isteklerin (Request Body) doğruluğunu Pydantic ile denetlemek ve bu verileri SQLModel (SQLAlchemy ve Pydantic'in birleşimi) kullanarak veritabanına asenkron olarak yazmak, okumak, güncellemek ve silmek (CRUD) uygulamanın kalbini oluşturur.
Gelişmiş Güvenlik ve Kimlik Doğrulama (JWT, RBAC & Redis): Sadece kullanıcı adı ve şifre doğrulaması yetmez. Sistemde oturum yönetimini güvenli kılmak için JWT (JSON Web Tokens) üretilir, yenileme token'ları (Refresh Token) yönetilir ve iptal edilen (log-out olan) token'lar Redis üzerinde tutularak anlık engellenir. Ayrıca RBAC (Rol Tabanlı Erişim Kontrolü) ile sistemdeki her uç noktanın (endpoint) sadece yetkili roller (Örn: Admin, User) tarafından tetiklenmesi sağlanır.
Arka Plan Görevleri ve Zamanlanmış İşler (Celery + Redis): E-posta gönderimi, büyük rapor oluşturma veya resim boyutlandırma gibi uzun süren işlemler kullanıcıyı tarayıcıda bekletmemelidir. Bu işler Celery aracılığıyla arka plana atılır ve mesaj kuyruğu olarak Redis kullanılır.
Test Otomasyonu ve Canlıya Alım (Deployment): Yazılan API'ların doğruluğu Pytest ve Schemathesis (mülkiyet/özellik tabanlı test) ile otomatik olarak test edilir. Son aşamada ise tüm bu ekosistem (FastAPI, PostgreSQL, Redis, Celery) production ortamına Docker veya bulut platformları üzerinden konuşlandırılır.
Bir FastAPI projesine başlarken en kritik adım, bağımlılıkların izole edildiği temiz bir sanal ortam (Virtual Environment) oluşturmak ve projenin iskeletini kurmaktır.
Sisteminizde Python yüklü olduğunu varsayarak terminal üzerinden şu adımları takip ederiz:
# 1. Sanal ortam oluşturmapython -m venv venv# 2. Sanal ortamı aktif etme (Windows için: venv\Scripts\activate)source venv/bin/activate# 3. Gerekli kütüphanelerin yüklenmesipip install fastapi uvicorn
Kök dizinde bir main.py
dosyası oluşturup projenin ilk ayağa kaldırılma kodunu yazarız:
# main.pyfromfastapiimportFastAPI# FastAPI nesnesini somutlaştırıyoruzapp= FastAPI(title="FastAPI Kapsamlı Kurs Projesi")@app.get("/")asyncdefroot():return{"mesaj":"FastAPI Uygulamasına Hoş Geldiniz!"}
Çalıştırma Komutu:
uvicorn main:app --reload
Açıklama:
--reload
parametresi, kodda bir değişiklik yaptığınızda uvicorn
sunucusunun otomatik olarak yenilenmesini sağlar. Bu, geliştirme
aşamasında büyük kolaylık sağlar.
İstemciden (Client) sunucuya veri göndermenin 4 temel yolu vardır: Path (Yol) Parametreleri, Query (Sorgu) Parametreleri, Request Body (İstek Gövdesi) ve Headers (Başlıklar).
URL yapısının ayrılmaz bir parçası olan, genellikle spesifik bir kaynağı işaret etmek için kullanılan parametrelerdir.
Örnek: https://magaza.com
adresindeki 45
bir yol parametresidir.
@app.get("/urunler/{urun_id}")asyncdefurun_detay_getir(urun_id: int):# FastAPI, urun_id'nin int olmasını zorunlu kılar.# Eğer url'e /urunler/abc yazılırsa otomatik olarak veri doğrulama hatası (422) fırlatır.return{"urun_id": urun_id,"durum":"Aktif","stok":120}
URL'de ?
işaretinden sonra gelen anahtar-değer çiftleridir. Genellikle
filtreleme, sıralama ve sayfalama (pagination) işlemlerinde
kullanılır.
Örnek:
/urunler?kategori=elektronik&sirala=fiyat
fromtypingimportOptional@app.get("/urunler")asyncdefurunleri_listele(kategori: Optional[str] =None,limit: int =10):# kategori opsiyoneldir, limit verilmezse varsayılan olarak 10 alınır.return{"filtre_kategorisi": kategori,"getirilen_adet": limit,"sonuc":f"{kategoriifkategorielse'Tüm'}kategorisindeki ürünler listelendi."}
İstemcinin sunucuya (genellikle POST, PUT, PATCH isteklerinde) göndermek istediği ham veridir. FastAPI bu veriyi yakalamak ve doğrulamak için Pydantic modellerini kullanır.
frompydanticimportBaseModel,Field# Gönderilecek verinin şablonunu (Şemasını) tanımlıyoruzclassUrunEklemeSeması(BaseModel):isim: str = Field(..., min_length=2, max_length=50)fiyat: float = Field(..., gt=0, description="Fiyat sıfırdan büyük olmalıdır.")stok_adet: int@app.post("/urunler/ekle")asyncdefyeni_urun_olustur(urun: UrunEklemeSeması):# İstemciden gelen JSON verisi otomatik olarak 'urun' nesnesine dönüştürülür.return{"mesaj":"Ürün başarıyla eklendi","urun_detay": urun.dict()}
İstekle ilgili meta verileri (Yetkilendirme token'ları, tarayıcı bilgileri vb.) taşımak için kullanılır.
fromfastapiimportHeader@app.get("/guvenlik/kontrol")asyncdefuser_agent_oku(user_agent: str = Header(None),x_custom_token: str = Header(None)):# HTTP başlığındaki 'User-Agent' ve 'X-Custom-Token' otomatik yakalanır.return{"Kullanıcı Tarayıcısı": user_agent,"Özel Token": x_custom_token}
Gerçek senaryolarda, istemciye veritabanındaki her veriyi (Örn: şifre karmaları, silinme logları) dönmek istemeyiz. Burada Response Models (Yanıt Modelleri) devreye girer. Ayrıca hatalı işlemlerde düzgün HTTP durum kodları ile Exception (İstisna) fırlatılması gerekir.
Aşağıdaki örnekte, henüz veritabanına geçmeden konunun mantığını kavramak için geçici bir Python listesi üzerinde tam bir CRUD senaryosu ve hata yönetimi uyguluyoruz:
fromfastapiimportHTTPException,status# Sahte veritabanı simülasyonuKURS_VERITABANI= [{"id":1,"baslik":"Python Giriş","egitmen":"Ahmet","gizli_not":"Maliyet: 100TL"},{"id":2,"baslik":"FastAPI İleri Seviye","egitmen":"Mehmet","gizli_not":"Maliyet: 200TL"}]# İstemciye sadece görmesi gereken alanları filtrelemek için Pydantic Modeli tanımlıyoruzclassKursYatitSemasi(BaseModel):id: intbaslik: stregitmen: str# 'gizli_not' alanını buraya eklemediğimiz için dış dünyaya bu veri asla sızmayacak.@app.get("/kurslar/{kurs_id}", response_model=KursYatitSemasi)asyncdefkurs_getir(kurs_id: int):forkursinKURS_VERITABANI:ifkurs["id"] == kurs_id:returnkurs# response_model sayesinde otomatik filtrelenir.# Eğer kurs bulunamazsa 404 hatası fırlatıyoruzraiseHTTPException(status_code=status.HTTP_404_NOT_FOUND,detail=f"{kurs_id}numaralı kurs sistemde bulunamadı!")
Tüm API uç noktalarını tek bir
dosyada toplamak projenin büyümesini engeller. FastAPI, kodları iş
mantıklarına göre parçalamak için APIRouter
sınıfını sunar.
src/│├── main.py # Uygulamanın ana giriş kapısı└── routers/├── __init__.py├── kullanıcılar.py # Kullanıcı işlemleri API'ları└── kitaplar.py # Kitap işlemleri API'ları
src/routers/kitaplar.py
dosyasının içeriği:
fromfastapiimportAPIRouterrouter= APIRouter(prefix="/kitaplar",tags=["Kitap İşlemleri"])@router.get("/")asyncdeftum_kitapları_getir():return[{"id":1,"ad":"Suç ve Ceza"}]
src/main.py
dosyasında bu router'ın ana uygulamaya dahil edilmesi:
fromfastapiimportFastAPIfromroutersimportkitaplar# Oluşturduğumuz modülü içeri alıyoruzapp= FastAPI()# Router'ı uygulamaya kaydediyoruzapp.include_router(kitaplar.router)
Bu işlemden sonra, tarayıcıdan
/kitaplar/
adresine gidildiğinde modüler olarak tasarladığımız uç nokta
çalışacaktır.
Harika, eğitim serimizin en kritik mimari taşlarından biri olan Databases & SQLModel (Part 5, Part 6, Part 7 ve Part 8) bölümüne geçiyoruz.
Bu bölümde veritabanı bağlantılarını asenkron (async) hale getirmeyi, FastAPI'nin modern Lifespan etkinlik yapısını, hem Pydantic şeması hem de SQL tablosu işlevi gören SQLModel yapısını, Dependency Injection (Bağımlılık Enjeksiyonu) mekanizmasını ve veritabanı şema değişikliklerini yöneten Alembic aracını tek bir satırı bile atlamadan, teorik ve pratik olarak ele alıyoruz.
Bu bölümün temel amacı, klasik ORM (Object-Relational Mapping) kütüphanelerinin (SQLAlchemy gibi) ve veri doğrulama kütüphanelerinin (Pydantic gibi) yarattığı kod tekrarını sıfıra indiren SQLModel kütüphanesini asenkron olarak projeye entegre etmektir.
Eski FastAPI sürümlerinde
veritabanı başlatma ve kapatma işlemleri için startup
ve shutdown
etkinlikleri kullanılıyordu. Modern FastAPI mimarisinde bu işlem,
uygulamanın tüm yaşam döngüsünü tek bir bağlam yöneticisi
(context manager) altında toplayan Lifespan
yapısı ile çözülür.
Öncelikle gerekli kütüphaneleri ortamımıza dahil etmemiz gerekir:
pip install sqlmodel asyncpg
(Not: asyncpg,
PostgreSQL ile asenkron iletişim kurmak için kullanılan güçlü
bir sürücüdür.)
src/db.py
(Veritabanı Yapılandırma Dosyası):
fromsqlalchemy.ext.asyncioimportAsyncSession,create_async_enginefromsqlalchemy.ormimportsessionmakerfromsqlmodelimportSQLModel# Asenkron PostgreSQL bağlantı dizesi# Gerçek senaryoda bu bilgi .env dosyasından okunmalıdır.ASYNC_DATABASE_URL="postgresql+asyncpg://kullanici:sifre@localhost:5432/fastapi_db"# Asenkron motoru (engine) oluşturuyoruzasync_engine= create_async_engine(ASYNC_DATABASE_URL,echo=True,# Veritabanında dönen SQL sorgularını terminale yazdırır (Geliştirme için ideal)future=True)# Tabloları veritabanında otomatik oluşturacak yardımcı fonksiyonasyncdefinit_db():asyncwithasync_engine.begin()asconn:# Kod tabanlı SQLModel modellerini okur ve veritabanında tabloları kurarawaitconn.run_sync(SQLModel.metadata.create_all)
src/main.py
(Lifespan Entegrasyonu):
fromcontextlibimportasynccontextmanagerfromfastapiimportFastAPIfromdbimportinit_db@asynccontextmanagerasyncdeflifespan(app: FastAPI):# Uygulama AYAĞA KALKARKEN çalışacak kısım:print("Uygulama başlatılıyor, veritabanı bağlantısı kuruluyor...")awaitinit_db()yield# Uygulama çalışırken burası askıda bekler# Uygulama KAPANIRKEN çalışacak kısım:print("Uygulama kapatılıyor, bağlantılar güvenli bir şekilde sonlandırılıyor...")app= FastAPI(lifespan=lifespan)
FastAPI’nin en güçlü
yanlarından biri Depends
fonksiyonu ile sağlanan Dependency Injection (Bağımlılık
Enjeksiyonu) mekanizmasıdır.
Her HTTP isteği geldiğinde yeni bir veritabanı oturumu (Session)
açılmasını, işlem bittiğinde ise bu oturumun otomatik olarak
kapatılmasını sağlar.
src/db.py
dosyasına eklenecek session fonksiyonu:
asyncdefget_session() -> AsyncSession:# Asenkron session fabrikası kuruyoruzasync_session= sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)asyncwithasync_session()assession:yieldsession# Oturumu teslim eder, endpoint işini bitirince buraya döner ve oturumu kapatır.
src/models.py
(SQLModel Tanımı):
fromtypingimportOptionalfromsqlmodelimportField,SQLModel# Bu sınıf hem bir Pydantic şeması hem de bir SQL tablosudur!classKurs(SQLModel, table=True):id: Optional[int] = Field(default=None, primary_key=True)baslik: str = Field(index=True)egitmen: strfiyat: float
src/routers/kurslar.py
(Asenkron CRUD Router):
fromfastapiimportAPIRouter,Depends,HTTPException,statusfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemy.futureimportselectfromdbimportget_sessionfrommodelsimportKursrouter= APIRouter(prefix="/kurslar", tags=["Kurs Yönetimi"])# 1. CREATE: Yeni Kurs Ekleme@router.post("/", status_code=status.HTTP_201_CREATED)asyncdefkurs_ekle(yeni_kurs: Kurs,session: AsyncSession = Depends(get_session)):session.add(yeni_kurs)awaitsession.commit()# Veritabanına kaydetawaitsession.refresh(yeni_kurs)# Veritabanında oluşan id'yi nesneye yüklereturnyeni_kurs# 2. READ: Tüm Kursları Listeleme@router.get("/")asyncdefkursları_listele(session: AsyncSession = Depends(get_session)):sorgu= select(Kurs)sonuc=awaitsession.execute(sorgu)tum_kurslar= sonuc.scalars().all()returntum_kurslar# 3. READ: Tek Bir Kurs Getirme@router.get("/{kurs_id}")asyncdefkurs_detay(kurs_id: int,session: AsyncSession = Depends(get_session)):kurs=awaitsession.get(Kurs, kurs_id)ifnotkurs:raiseHTTPException(status_code=404, detail="Kurs bulunamadı")returnkurs
Üretim ortamındaki
veritabanlarında SQLModel.metadata.create_all
kullanmak tehlikelidir çünkü mevcut tabloları güncelleyemez,
sadece sıfırdan oluşturur. Tabloya yeni bir kolon eklendiğinde
verileri kaybetmeden veritabanını güncellemek için Alembic
kullanılır.
Kullanıcı kayıt ve giriş işlemlerinde kullanacağımız temel modeli tanımlayalım.
src/models.py
(Kullanıcı Tablosu):
importdatetimefromsqlmodelimportField,SQLModelclassUser(SQLModel, table=True):id: int = Field(default=None, primary_key=True): str = Field(unique=True, index=True, nullable=False)password_hash: str = Field(nullable=False)# Şifreleri asla düz metin olarak tutmuyoruzis_active: bool = Field(default=True)is_superuser: bool = Field(default=False)created_at: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
Terminal üzerinden Alembic yapılandırmasını başlatıyoruz:
# Alembic kütüphanesini kurunpip install alembic# Projede alembic ortamını başlatınalembic init alembic
Bu komuttan sonra proje kök
dizininde alembic.ini
dosyası ve alembic/
klasörü oluşur. Alembic'in asenkron projemizi ve SQLModel yapımızı
tanıyabilmesi için iki kritik dosya düzenlemesi yapmalıyız:
alembic.ini
dosyasında veritabanı adresini ayarlayın:
sqlalchemy.url = postgresql+asyncpg://kullanici:sifre@localhost:5432/fastapi_db
alembic/env.py
dosyasında modellerimizi ve hedefi tanıtın:
# env.py dosyasının üst kısımlarına ekleyin:fromsqlmodelimportSQLModelfrommodelsimportUser,Kurs# Tüm modellerimizi içeri aktarıyoruz# target_metadata satırını bulup şu şekilde güncelleyin:target_metadata= SQLModel.metadata
İlk Migrasyonu Üretme ve Veritabanına Uygulama Komutları:
# Değişiklikleri tespit edip otomatik migrasyon dosyası oluştururalembic revision --autogenerate -m"kullanici ve kurs tablolari eklendi"# Oluşan bu şemayı gerçek veritabanına uygular (Tabloları basar)alembic upgrade head
Kullanıcı hesapları oluşturulurken şifrelerin güvenliği en kritik konudur. Şifreler veritabanına kaydedilmeden önce tek yönlü kriptografik algoritmalardan (örneğin bcrypt) geçirilerek "hash"lenmelidir.
Önce şifreleme kütüphanesini kuralım:
pip install "passlib[bcrypt]"
src/security.py
(Güvenlik Yardımcı Dosyası):
frompasslib.contextimportCryptContext# Bcrypt algoritmasını kullanacak bağlamı tanımlıyoruzpwd_context= CryptContext(schemes=["bcrypt"], deprecated="auto")defhash_password(password: str) -> str:returnpwd_context.hash(password)defverify_password(plain_password: str,hashed_password: str) -> bool:returnpwd_context.verify(plain_password, hashed_password)
src/schemas.py
(Giriş/Çıkış Şemaları):
İstemcinin
şifresini alırken kullanacağımız ama veritabanındaki hash
değerini dışarı sızdırmayacak ara şemalar (DTO - Data Transfer
Object):
frompydanticimportBaseModel,EmailStrclassUserCreateSchema(BaseModel):: EmailStrpassword: str# Kullanıcının ham şifresi (istek esnasında)classUserResponseSchema(BaseModel):id: int: EmailStris_active: bool# Şifre alanı burada yok! Güvenli çıktı sağlar.
src/routers/auth.py
(Kullanıcı Kayıt API Uç Noktası):
fromfastapiimportAPIRouter,Depends,HTTPException,statusfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemy.futureimportselectfromdbimportget_sessionfrommodelsimportUserfromschemasimportUserCreateSchema,UserResponseSchemafromsecurityimporthash_passwordrouter= APIRouter(prefix="/auth", tags=["Kimlik Doğrulama"])@router.post("/register", response_model=UserResponseSchema, status_code=status.HTTP_201_CREATED)asyncdefregister_user(user_data: UserCreateSchema,session: AsyncSession = Depends(get_session)):# 1. Email adresi sistemde zaten var mı kontrol etsorgu= select(User).where(User.email == user_data.email)sonuc=awaitsession.execute(sorgu)mevcut_kullanici= sonuc.scalars().first()ifmevcut_kullanici:raiseHTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail="Bu e-posta adresiyle zaten bir kullanıcı kayıtlı.")# 2. Şifreyi hash'le ve yeni kullanıcı nesnesini oluşturyeni_kullanici= User(email=user_data.email,password_hash=hash_password(user_data.password)# Güvenli hashleme)# 3. Veritabanına kaydetsession.add(yeni_kullanici)awaitsession.commit()awaitsession.refresh(yeni_kullanici)returnyeni_kullanici# Şema sayesinde sadece güvenli alanlar döner
Harika! Projemizin en kritik güvenlik duvarını öreceğimiz Authentication & User Management (Part 9, Part 10, Part 11, Part 12 ve Part 13) bölümüne geçiyoruz.
Bu bölümde endüstri standardı
olan JWT (JSON Web Tokens)
mimarisini kuracak, kısa ömürlü Access
Token
ile uzun ömürlü Refresh
Token
mekanizmasını inşa edecek, API uç noktalarımızı HTTP
Bearer Auth ile koruma altına
alacağız. Ayrıca kullanıcı çıkış yaptığında token'ları
anında geçersiz kılmak için Redis tabanlı bir Token
Revocation (İptal) sistemi
tasarlayacak ve en sonunda Rol Tabanlı Erişim Kontrolü
(RBAC) uygulayacağız.
Bu katmanda amaç; kullanıcının kimliğini doğrulamak, ona bir kimlik kartı (Token) teslim etmek ve bu kartın yetki düzeyine (Rollerine) göre API içerisindeki kapılardan geçişini kontrol etmektir.
JWT, taraflar arasında güvenli bir şekilde JSON nesnesi olarak veri taşımaya yarayan açık bir standarttır (RFC 7519).
Access Token: API isteklerinde kimlik doğrulamak için kullanılır. Güvenlik gerekçesiyle ömrü çok kısadır (Örn: 15 dakika).
Refresh Token: Access Token'ın süresi bittiğinde, kullanıcının tekrar şifre girmesine gerek kalmadan yeni bir Access Token almasını sağlar. Ömrü uzundur (Örn: 7 gün).
Gerekli kütüphaneyi ortamımıza dahil edelim:
pip install PyJWT
src/config.py
(Güvenlik Ayarları):
importosclassSecurityConfig:# Gerçek üretim ortamında bu anahtarlar çok güçlü rastgele dizgiler olmalı ve .env dosyasından okunmalıdır.JWT_SECRET="ust_dizey_gizli_anahtar_987654321"JWT_ALGORITHM="HS256"ACCESS_TOKEN_EXPIRE_MINUTES=15REFRESH_TOKEN_EXPIRE_DAYS=7
src/auth_utils.py
(Token Oluşturma Fonksiyonları):
importdatetimeimportjwtfromconfigimportSecurityConfigdefcreate_jwt_token(data: dict,expires_delta: datetime.timedelta) -> str:"""Genel JWT üreten yardımcı fonksiyon"""payload= data.copy()expire= datetime.datetime.utcnow() + expires_delta# 'exp' (expiration time) JWT standartlarında rezerve edilmiş bir anahtardır.payload.update({"exp": expire})encoded_jwt= jwt.encode(payload,SecurityConfig.JWT_SECRET,algorithm=SecurityConfig.JWT_ALGORITHM)returnencoded_jwtdefgenerate_tokens(user_id: int) -> dict:"""Kullanıcı için hem Access hem de Refresh Token üretir"""access_token_expires= datetime.timedelta(minutes=SecurityConfig.ACCESS_TOKEN_EXPIRE_MINUTES)refresh_token_expires= datetime.timedelta(days=SecurityConfig.REFRESH_TOKEN_EXPIRE_DAYS)access_token= create_jwt_token(data={"sub": str(user_id),"token_type":"access"},expires_delta=access_token_expires)refresh_token= create_jwt_token(data={"sub": str(user_id),"token_type":"refresh"},expires_delta=refresh_token_expires)return{"access_token": access_token,"refresh_token": refresh_token,"token_type":"bearer"}
Bir istemci korumalı bir API uç
noktasına istek attığında, token bilgisini HTTP başlığında
(Authorization:
Bearer <token>)
göndermek zorundadır. FastAPI, bu başlığı yakalamak için
yerleşik HTTPBearer
güvenlik şemasını sunar.
src/auth_utils.py
dosyasına eklenecek doğrulama katmanı:
importjwtfromfastapiimportDepends,HTTPException,statusfromfastapi.securityimportHTTPBearer,HTTPAuthorizationCredentialsfromconfigimportSecurityConfig# FastAPI'nin HTTPBearer şemasını somutlaştırıyoruzsecurity_scheme= HTTPBearer()asyncdefget_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security_scheme)) -> int:"""Gelen istekteki Bearer token'ı yakalar, çözer ve doğrular.Başarılıysa token içindeki kullanıcı ID'sini (sub) döner."""token= credentials.credentialstry:# Token şifresini çöz ve geçerliliğini (ve süresini) kontrol etpayload= jwt.decode(token,SecurityConfig.JWT_SECRET,algorithms=[SecurityConfig.JWT_ALGORITHM])# Token tipinin 'access' olduğundan emin oluyoruzifpayload.get("token_type") !="access":raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Geçersiz token türü. Lütfen Access Token kullanın.")user_id= payload.get("sub")ifuser_idisNone:raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Token içeriği geçersiz (sub bulunamadı).")returnint(user_id)exceptjwt.ExpiredSignatureError:raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Token süresi dolmuş. Lütfen token'ı yenileyin.")exceptjwt.PyJWTError:raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Geçersiz kimlik doğrulama token'ı.")
Korumalı Uç Nokta Örneği (src/routers/profil.py):
fromfastapiimportAPIRouter,Dependsfromauth_utilsimportget_current_user_idrouter= APIRouter(prefix="/profil", tags=["Kullanıcı Profili"])# Bu endpoint'e erişmek isteyen biri geçerli token sunmak zorundadır@router.get("/me")asyncdefprofil_detayi(current_user_id: int = Depends(get_current_user_id)):return{"mesaj":"Gizli profile erişim sağlandı.","kullanici_id": current_user_id}
Access token'ın süresi bittiğinde
istemci (örneğin mobil uygulama) arka planda /auth/refresh
uç noktasına elindeki Refresh
Tokenı
göndererek sessizce yeni bir Access
Token
talep eder.
src/routers/auth.py
dosyasına eklenecek yenileme uç noktası:
importjwtfromfastapiimportAPIRouter,HTTPException,status,Bodyfromauth_utilsimportcreate_jwt_token,SecurityConfigimportdatetime@router.post("/refresh")asyncdefrefresh_access_token(refresh_token: str = Body(..., embed=True)):try:# Refresh token'ı çözüyoruzpayload= jwt.decode(refresh_token,SecurityConfig.JWT_SECRET,algorithms=[SecurityConfig.JWT_ALGORITHM])ifpayload.get("token_type") !="refresh":raiseHTTPException(status_code=400, detail="Bu bir refresh token değil.")user_id= payload.get("sub")# Sadece yeni bir kısa ömürlü Access Token üretiyoruzaccess_token_expires= datetime.timedelta(minutes=SecurityConfig.ACCESS_TOKEN_EXPIRE_MINUTES)new_access_token= create_jwt_token(data={"sub": user_id,"token_type":"access"},expires_delta=access_token_expires)return{"access_token": new_access_token,"token_type":"bearer"}exceptjwt.PyJWTError:raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Geçersiz veya süresi dolmuş yenileme token'ı.")
JWT'ler doğası gereği "stateless" (durumsuz) yapılardır; sunucu veritabanına bakmadan token'ın doğruluğunu anlar. Ancak kullanıcı "Çıkış Yap" (Logout) dediğinde, o token süresi dolana kadar hala geçerlidir. Bu güvenlik açığını kapatmak için iptal edilen token'ları (Blocklist) çok hızlı okuma/yazma yapan Redis üzerinde saklarız.
Önce Redis kütüphanesini kuralım:
pip install redis
src/redis_client.py
(Redis Bağlantısı):
importredis# Redis sunucusuna bağlanıyoruz (Varsayılan port 6379)redis_db= redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)defblock_token(jti: str,expire_seconds: int):"""Token'ın benzersiz kimliğini (jti veya token ham halini) Redis'e ekler"""redis_db.setex(name=jti, time=expire_seconds, value="true")defis_token_blocked(jti: str) -> bool:"""Token Redis blocklist'inde var mı kontrol eder"""returnredis_db.exists(jti) ==1
src/auth_utils.py
içerisindeki get_current_user_id
fonksiyonuna eklenecek Redis Kontrolü:
# get_current_user_id fonksiyonunun içine, jwt.decode işleminden HEMEN SONRA eklenmelidir:# Basitlik açısından token dizgesinin kendisini anahtar olarak kontrol edelim:ifis_token_blocked(token):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Bu token iptal edilmiş (Kullanıcı çıkış yaptı).")
Kullanıcı Çıkış Yapma Uç Noktası (src/routers/auth.py):
@router.post("/logout")asyncdeflogout(credentials: HTTPAuthorizationCredentials = Depends(security_scheme)):token= credentials.credentials# Token'ı 15 dakika boyunca (Access token ömrü kadar) Redis'te karantinaya alıyoruzblock_token(jti=token, expire_seconds=15*60)return{"mesaj":"Başarıyla çıkış yapıldı, token iptal edildi."}
Sistemdeki bazı uç noktalara
sadece belirli yetkideki kişilerin (Örn: admin
veya instructor)
erişmesi istenir. Bunu sağlamak için FastAPI'nin dinamik sınıf
tabanlı bağımlılık yapısını (Callable Dependencies)
kullanırız.
Öncelikle kullanıcı modelimize rol alanı eklememiz gerekir.
src/models.py
(Genişletilmiş User Tablosu):
# User sınıfına yeni bir alan ekliyoruz (Alembic ile veritabanına yansıtılmalı)classUser(SQLModel, table=True):# ... eski alanlar (id, email, password_hash)role: str = Field(default="user")# Seçenekler: user, instructor, admin
Her endpoint için ayrı fonksiyon yazmak yerine, izin verilen rolleri dinamik olarak kabul eden bir sınıf tasarlıyoruz.
src/rbac.py
(Rol Kontrol Mekanizması):
fromfastapiimportDepends,HTTPException,statusfromsqlalchemy.ext.asyncioimportAsyncSessionfromdbimportget_sessionfrommodelsimportUserfromauth_utilsimportget_current_user_idclassRoleChecker:def__init__(self,allowed_roles: list[str]):# İzin verilen rolleri sınıf başlatılırken alıyoruzself.allowed_roles = allowed_rolesasyncdef__call__(self,user_id: int = Depends(get_current_user_id),session: AsyncSession = Depends(get_session)):"""İstek geldiğinde FastAPI bu metodu tetikler"""# Veritabanından kullanıcının güncel rolünü sorgulauser=awaitsession.get(User, user_id)ifnotuseroruser.rolenotinself.allowed_roles:raiseHTTPException(status_code=status.HTTP_03_FORBIDDEN,# 403 Forbiddendetail="Bu işlemi gerçekleştirmek için yetkiniz yetersiz.")returnuser# Yetki tamamsa kullanıcı nesnesini teslim et
src/routers/kurslar.py
(Sadece Eğitmen ve Adminlerin Kurs Ekleyebilmesi):
fromfastapiimportAPIRouter,DependsfromrbacimportRoleCheckerfrommodelsimportKurs,Userrouter= APIRouter(prefix="/kurslar", tags=["Kurs Yönetimi"])# Sadece admin ve instructor rollerine izin veren denetleyici nesnesiallow_management= RoleChecker(["admin","instructor"])@router.post("/ekle")asyncdefkurs_ekle(yeni_kurs: Kurs,current_admin: User = Depends(allow_management)):# Eğer istek atan kullanıcı admin veya instructor değilse yukarıdaki Depends adımında elenecektir.return{"mesaj":"Yetki doğrulandı, kurs başarıyla sisteme eklendi.","ekleyen_yetkili": current_admin.email}
Veri tabanımızın kalbini oluşturacak ve tabloları birbirine akıllı bağlarla bağlayacağımız Databases Relationships & Advanced SQLModel (Part 14 ve Part 15) bölümüne geçiyoruz.
Bu bölümde, ilişkisel veri
tabanlarının en önemli konusu olan İlişki
Tanımlamalarını (Relationships)
ele alacağız. Bir eğitmenin birden fazla kursa sahip olduğu
Bire-Çok (One-to-Many)
ilişki modelini kuracak, SQLModel kütüphanesinin en güçlü
özelliklerinden olan Relationship
nesnesini kullanacak ve verilerin birbirini sonsuz döngüye
sokmasını engelleyen gelişmiş İçiçe Pydantic
Şemaları (Advanced Schemas)
tasarlayacağız.
SQLModel kullanırken ilişkileri iki aşamalı düşünmek gerekir:
Foreign Key (Yabancı Anahtar): SQL düzeyinde tabloları birbirine bağlayan gerçek veri tabanı sütunudur.
Relationship Property (İlişki Özelliği): Python/ORM düzeyinde, sanki nesnenin bir özelliğiymiş gibi ilişkili diğer nesnelere (kod yazarken kolayca) erişmemizi sağlayan sanal bağlantıdır.
Senaryomuzda bir Eğitmen
(User) birden fazla Kurs
açabilir. Yani User
ve Kurs
arasında Bire-Çok (One-to-Many) bir ilişki mevcuttur.
src/models.py
(Güncellenmiş Modeller):
fromtypingimportList,OptionalfromsqlmodelimportField,Relationship,SQLModelclassUser(SQLModel, table=True):id: Optional[int] = Field(default=None, primary_key=True): str = Field(unique=True, index=True, nullable=False)password_hash: str = Field(nullable=False)role: str = Field(default="user")# ORM İlişkisi: Bir kullanıcının açtığı tüm kurslara 'user.kurslar' diyerek erişebileceğiz.# back_populates, Kurs modelindeki 'egitmen' özelliğine işaret eder.kurslar: List["Kurs"] = Relationship(back_populates="egitmen")classKurs(SQLModel, table=True):id: Optional[int] = Field(default=None, primary_key=True)baslik: str = Field(index=True)fiyat: float# SQL Düzeyinde Foreign Key (Yabancı Anahtar) Tanımı# Bu alan, kursun hangi eğitmene ait olduğunun ID bilgisini tutar.egitmen_id: int = Field(foreign_key="user.id", nullable=False)# ORM İlişkisi: Bir kursun eğitmen bilgilerine 'kurs.egitmen' diyerek doğrudan erişebileceğiz.egitmen: User = Relationship(back_populates="kurslar")
İlişkili modelleri doğrudan API
yanıtı (Response) olarak döndürmeye çalıştığımızda çok
büyük bir sorunla karşılaşırız: Sonsuz Döngü
(Infinite Recursion Loop).
Kurs
nesnesi Eğitmen nesnesini yüklemeye çalışır $\rightarrow$
Eğitmen nesnesi içindeki Kurslar listesini yüklemeye çalışır
$\rightarrow$ O kurslar tekrar Eğitmeni yükler...
Bu döngü sistemin çökmesine neden olur.
Bu sorunu çözmek ve temiz bir API çıktısı sunmak için Gelişmiş İçiçe Pydantic Şemaları (Nested Schemas) tasarlamamız gerekir.
src/schemas.py
(Döngü Engelleyici Temiz Şemalar):
fromtypingimportListfrompydanticimportBaseModel,EmailStr# --- TEMEL ŞEMALAR (İlişkisiz, Yalın Veriler) ---classUserBaseSchema(BaseModel):id: int: EmailStrrole: strclassKursBaseSchema(BaseModel):id: intbaslik: strfiyat: floategitmen_id: int# --- GELİŞMİŞ İLİŞKİLİ ŞEMALAR (İçiçe Yapılar) ---classKursDetayResponseSchema(KursBaseSchema):"""Bir kurs çağrıldığında, yanında eğitmeninin bilgisini de döner"""egitmen: UserBaseSchema# İçiçe pydantic şeması kullanımıclassEgitmenDetayResponseSchema(UserBaseSchema):"""Bir eğitmen çağrıldığında, açtığı tüm kursların listesini de döner"""kurslar: List[KursBaseSchema]
İlişkili verileri asenkron veri tabanından çekerken SQLAlchemy varsayılan olarak ilişkili tabloları otomatik yüklemez (Lazy Loading aktiftir ve asenkron mimaride hata verir). Bu yüzden sorgu esnasında ilişkili tabloları agresif bir şekilde yüklememiz (Eager Loading / selectinload) gerekir.
src/routers/kurslar.py
(İlişkili Veri Getiren API):
fromfastapiimportAPIRouter,Depends,HTTPExceptionfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemy.futureimportselectfromsqlalchemy.ormimportselectinloadfromdbimportget_sessionfrommodelsimportKursfromschemasimportKursDetayResponseSchemarouter= APIRouter(prefix="/kurslar", tags=["Kurs İlişkileri"])@router.get("/{kurs_id}", response_model=KursDetayResponseSchema)asyncdefilişkili_kurs_detayi_getir(kurs_id: int,session: AsyncSession = Depends(get_session)):# selectinload kullanarak Kurs verisini çekerken, ona bağlı olan 'egitmen' verisini de tek seferde çekiyoruz.sorgu= select(Kurs).where(Kurs.id == kurs_id).options(selectinload(Kurs.egitmen))sonuc=awaitsession.execute(sorgu)kurs= sonuc.scalars().first()ifnotkurs:raiseHTTPException(status_code=404, detail="Aranan kurs bulunamadı.")returnkurs# KursDetayResponseSchema şemasına tam uyumlu ve güvenli bir JSON çıktısı üretilir.
Üretilecek Örnek API JSON Çıktısı (Sonsuz döngüden uzak, temiz çıktı):
{"id":10,"baslik":"FastAPI İleri Seviye","fiyat":299.99,"egitmen_id":3,"egitmen": {"id":3,"email":"ahmet@egitim.com","role":"instructor"}}
Uygulamamızın kararlılığını, güvenliğini ve izlenebilirliğini (observability) en üst seviyeye çıkaracağımız Error Handling & Middleware (Part 16 ve Part 17) bölümüne geçiyoruz.
Bu bölümde, API boyunca standart bir hata formatı yakalamak için Özel API İstisnaları (Custom API Exceptions) geliştirecek, gelen her isteği ve oluşan hataları takip etmek amacıyla bir Custom Logging (Özel Günlükleme) mekanizması kuracak ve son olarak web uygulamalarının (React, Vue, Angular vb.) API'ımıza güvenli bir şekilde erişebilmesi için hayati önem taşıyan CORS (Cross-Origin Resource Sharing) ayarlarını yapılandıracağız.
Bir API'ın profesyonelliği, sadece başarılı senaryolarda değil, hata anlarında istemciye ne kadar açıklayıcı ve standart bir yanıt döndüğüyle de ölçülür. Middleware (Ara Yazılım) katmanı ise isteklerin ve yanıtların arasına girerek küresel operasyonlar yürütmemizi sağlar.
FastAPI'nin yerleşik HTTPException
sınıfı iş görse de, kurumsal projelerde tüm hataların
istemciye aynı JSON şablonuyla dönmesi istenir. Bunun için kendi
özel hata sınıflarımızı türetir ve bunları FastAPI'nin
küresel Exception Handler (İstisna Yakalayıcı)
mekanizmasına kaydederiz.
src/exceptions.py
(Özel İstisna Sınıfları ve Handler Tanımı):
fromfastapiimportRequest,FastAPIfromfastapi.responsesimportJSONResponseimportdatetime# Tüm özel iş mantığı hatalarımız için bir taban sınıf oluşturuyoruzclassProjeTabanHata(Exception):def__init__(self,mesaj: str,durum_kodu: int =400):self.mesaj = mesajself.durum_kodu = durum_kodu# Taban sınıftan türeyen spesifik hata sınıflarıclassKaynakBulunamadiHatasi(ProjeTabanHata):def__init__(self,mesaj: str ="Aranan kaynak bulunamadı."):super().__init__(mesaj, durum_kodu=404)classYetersizBakiyeHatasi(ProjeTabanHata):def__init__(self,mesaj: str ="Bu işlem için bakiyeniz yetersiz."):super().__init__(mesaj, durum_kodu=400)defkaydet_ozel_hata_yakalayicilar(app: FastAPI):"""Yazdığımız özel hataları FastAPI'ye küresel olarak tanıtan fonksiyon"""@app.exception_handler(ProjeTabanHata)asyncdefproje_hata_yakalayici(request: Request,exc: ProjeTabanHata):# İstemciye dönecek standart hata şablonunu tasarlıyoruzreturnJSONResponse(status_code=exc.durum_kodu,content={"basarili":False,"hata": {"mesaj": exc.mesaj,"kod": exc.__class__.__name__,"zaman_damgasi": datetime.datetime.utcnow().isoformat()}})
Uygulama İçi Kullanım Örneği (src/routers/kurslar.py):
fromexceptionsimportKaynakBulunamadiHatasi@router.get("/detay/{kurs_id}")asyncdefkurs_kontrol_et(kurs_id: int):# Simülasyon: Kurs bulunamadı varsayalımkurs_var_mi=Falseifnotkurs_var_mi:# Kendi yazdığımız özel hatayı fırlatıyoruzraiseKaynakBulunamadiHatasi(f"{kurs_id}ID'li kurs sistemde mevcut değil.")
İstemciye Dönen Standart JSON Çıktısı:
{"basarili":false,"hata": {"mesaj":"45 ID'li kurs sistemde mevcut değil.","kod":"KaynakBulunamadiHatasi","zaman_damgasi":"2026-08-29T15:23:00"}}
API'ımıza gelen her HTTP isteğinin hangi adrese geldiğini, ne kadar sürede tamamlandığını ve hangi HTTP durum koduyla döndüğünü log dosyalarına veya terminale basmak, üretim ortamındaki hataları ayıklamak (debug) için kritiktir.
src/middleware.py
(Zamanlama ve Loglama Ara Yazılımı):
importtimeimportloggingfromfastapiimportRequest# Python yerleşik logging mekanizmasını yapılandırıyoruzlogging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")logger= logging.getLogger("API_Gozlemci")asyncdeflog_istek_sureleri_middleware(request: Request,call_next):"""Gelen isteklerin çalışma süresini hesaplayan ve loglayan ara yazılım"""baslangic_zamani= time.time()# İsteğin yolunu ve metodunu yakalıyoruzistek_yolu= request.url.pathistek_metodu= request.method# İsteği bir sonraki aşamaya (route handler'a) gönderiyoruz ve yanıtı alıyoruzyanit=awaitcall_next(request)gecen_sure= time.time() - baslangic_zamani# Bilgileri terminale/log dosyasına yazıyoruzlogger.info(f"Metod:{istek_metodu}| Yol:{istek_yolu}| Durum:{yanit.status_code}| Süre:{gecen_sure:.4f}sn")# İstemciye özel bir performans başlığı (header) da ekleyebilirizyanit.headers["X-Islem-Suresi"] = str(gecen_sure)returnyanit
Tarayıcılar, güvenlik nedeniyle
bir web sitesinin (Örn: http://localhost:3000
adresindeki bir React uygulamasının) farklı bir kökteki API'a
(Örn: http://projem.com)
doğrudan istek atmasını engeller. API'ımızın güvenli dış
kaynaklara kapılarını açması için CORS ayarlarını yapmalıyız.
src/main.py
(Entegrasyon):
fromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddlewarefromexceptionsimportkaydet_ozel_hata_yakalayicilarfrommiddlewareimportlog_istek_sureleri_middlewareapp= FastAPI()# --- 1. CORS AYARLARI ---# API'ımıza erişmesine izin verdiğimiz dış kökenlerin (origin) listesiorigins= ["http://localhost:3000",# Yerel React/Vue geliştirme sunucusu"https://projem.com",# Canlıdaki ön yüz (frontend) domaini]app.add_middleware(CORSMiddleware,allow_origins=origins,# İzin verilen adres listesiallow_credentials=True,# Çerezlerin (Cookies) taşınmasına izin verallow_methods=["*"],# Tüm HTTP metodlarına (GET, POST, PUT, DELETE vb.) izin verallow_headers=["*"],# Tüm HTTP başlıklarına izin ver)# --- 2. CUSTOM LOGGING MIDDLEWARE ENTEGRASYONU ---app.middleware("http")(log_istek_sureleri_middleware)# --- 3. CUSTOM ERROR HANDLER ENTEGRASYONU ---kaydet_ozel_hata_yakalayicilar(app)@app.get("/test")asyncdeftest_endpoint():return{"durum":"Her şey yolunda, log mekanizması devrede!"}
Bu yapılandırma tamamlandığında, terminalinizde her istekten sonra şu şekilde profesyonel log satırları akmaya başlayacaktır:
2026-08-29 15:23:45,123 - INFO - Metod: GET | Yol: /test | Durum: 200 | Süre: 0.0024 sn2026-08-29 15:24:02,541 - INFO - Metod: GET | Yol: /kurslar/detay/45 | Durum: 404 | Süre: 0.0081 sn
Güvenli bir web uygulamasının en kritik kullanıcı döngülerini oluşturan Security Features & User Flows (Part 18 ve Part 19) bölümüne geçiyoruz.
Bu bölümde, sahte hesap açılışlarını engellemek için E-posta ile Hesap Doğrulama (Email Verification) sistemini inşa edecek ve şifresini unutan kullanıcılar için güvenli, süreli ve tek kullanımlık token tabanlı Şifre Sıfırlama (Password Reset) akışını tasarlayacağız.
Kullanıcı kayıt akışlarında e-posta doğrulaması ve şifre sıfırlama süreçleri tasarlanırken güvenlik açığı yaratmamak adına şu kurallara dikkat edilmelidir:
Süreli Token Kullanımı: E-postaya giden onaylama ve şifre sıfırlama linkleri sonsuza kadar geçerli olmamalıdır (Örn: 24 saat veya 15 dakika geçerlilik).
Güvenli Arka Plan İletişimi: E-posta gönderme işlemi zaman alan bir iştir. Bu işlemler (müfredatın sonraki bölümünde göreceğimiz Celery entegrasyonu öncesi) kullanıcıyı arayüzde bekletmemek adına asenkron veya arka plan görevi (Background Tasks) olarak planlanmalıdır.
Kullanıcı ilk kayıt olduğunda
sistem durumu is_active
= False
olarak işaretlenir. Kullanıcının e-posta adresine benzersiz,
kriptografik bir doğrulama token'ı gönderilir. Kullanıcı bu
linke tıkladığında hesabı aktif hale getirilir.
Gerekli e-posta kütüphanesini kuralım:
pip install fastapi-mail
src/models.py
(Kullanıcı Tablosu Doğrulama Güncellemesi):
fromsqlmodelimportField,SQLModelfromtypingimportOptionalclassUser(SQLModel, table=True):id: Optional[int] = Field(default=None, primary_key=True): str = Field(unique=True, index=True)password_hash: stris_active: bool = Field(default=False)# İlk kayıt anında hesap pasiftir!
src/auth_utils.py
(Doğrulama Token'ı Üretme ve Çözme):
importjwtimportdatetimefromconfigimportSecurityConfigdefcreate_verification_token(: str) -> str:"""E-posta doğrulaması için 24 saat geçerli özel token üretir"""expire= datetime.datetime.utcnow() + datetime.timedelta(hours=24)payload= {"sub": email,"action":"email_verification","exp": expire}returnjwt.encode(payload, SecurityConfig.JWT_SECRET, algorithm=SecurityConfig.JWT_ALGORITHM)
src/routers/auth.py
(Kayıt ve Onaylama API Uç Noktaları):
fromfastapiimportAPIRouter,Depends,HTTPException,status,BackgroundTasksfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlalchemy.futureimportselectfromdbimportget_sessionfrommodelsimportUserfromschemasimportUserCreateSchemafromsecurityimporthash_passwordfromauth_utilsimportcreate_verification_token,SecurityConfigimportjwtrouter= APIRouter(prefix="/auth", tags=["Hesap Doğrulama"])# Sahte e-posta gönderme fonksiyonu (Simülasyon)defsend_simulation_email(: str,token: str):verification_link=f"https://projem.com{token}"print(f"\n[E-POSTA SİMÜLASYONU] Kime:{email}")print(f"Lütfen hesabınızı onaylamak için tıklayın:{verification_link}\n")@router.post("/register")asyncdefregister_user(user_data: UserCreateSchema,background_tasks: BackgroundTasks,session: AsyncSession = Depends(get_session)):# Email kontrolüsorgu= select(User).where(User.email == user_data.email)sonuc=awaitsession.execute(sorgu)ifsonuc.scalars().first():raiseHTTPException(status_code=400, detail="E-posta zaten kayıtlı.")yeni_kullanici= User(email=user_data.email, password_hash=hash_password(user_data.password), is_active=False)session.add(yeni_kullanici)awaitsession.commit()# Doğrulama token'ı oluşturmatoken= create_verification_token(user_data.email)# E-posta gönderme işlemini arka plan görevi olarak ekliyoruz (API'ı yavaşlatmaz)background_tasks.add_task(send_simulation_email, user_data.email, token)return{"mesaj":"Kayıt başarılı. Lütfen e-posta adresinize gönderilen onay linkine tıklayın."}@router.get("/verify-email")asyncdefverify_email(token: str,session: AsyncSession = Depends(get_session)):try:payload= jwt.decode(token, SecurityConfig.JWT_SECRET, algorithms=[SecurityConfig.JWT_ALGORITHM])ifpayload.get("action") !="email_verification":raiseHTTPException(status_code=400, detail="Geçersiz işlem türü.")= payload.get("sub")# Kullanıcıyı bul ve aktifleştirsorgu= select(User).where(User.email == email)sonuc=awaitsession.execute(sorgu)user= sonuc.scalars().first()ifnotuser:raiseHTTPException(status_code=404, detail="Kullanıcı bulunamadı.")ifuser.is_active:return{"mesaj":"Hesap zaten daha önce doğrulanmış."}user.is_active =True# Kullanıcı artık sistemi kullanabilirawaitsession.commit()return{"mesaj":"E-posta adresiniz başarıyla doğrulandı! Giriş yapabilirsiniz."}exceptjwt.ExpiredSignatureError:raiseHTTPException(status_code=400, detail="Doğrulama linkinin süresi dolmuş (24 saat).")exceptjwt.PyJWTError:raiseHTTPException(status_code=400, detail="Geçersiz doğrulama token'ı.")
Kullanıcı şifresini unuttuğunda, eski şifreyi ona göstermek kesinlikle yasaktır (Zaten bcrypt ile tek yönlü hash'lendiği için geri döndürülemez). Güvenli akışta kullanıcı e-postasını girer, ona 15 dakika ömürlü bir sıfırlama token'ı gönderilir. Kullanıcı bu token ile yeni şifresini belirler.
src/auth_utils.py
(Şifre Sıfırlama Token'ı):
defcreate_password_reset_token(: str) -> str:"""Şifre sıfırlama için sadece 15 dakika geçerli çok sıkı bir token üretir"""expire= datetime.datetime.utcnow() + datetime.timedelta(minutes=15)payload= {"sub": email,"action":"password_reset","exp": expire}returnjwt.encode(payload, SecurityConfig.JWT_SECRET, algorithm=SecurityConfig.JWT_ALGORITHM)
src/routers/auth.py
(Şifre Unuttum ve Yeni Şifre Kaydetme API'ları):
frompydanticimportBaseModel,EmailStrfromsecurityimporthash_passwordclassPasswordResetRequestSchema(BaseModel):: EmailStrclassPasswordResetConfirmSchema(BaseModel):token: strnew_password: str@router.post("/password-reset-request")asyncdefpassword_reset_request(data: PasswordResetRequestSchema,background_tasks: BackgroundTasks,session: AsyncSession = Depends(get_session)):sorgu= select(User).where(User.email == data.email)sonuc=awaitsession.execute(sorgu)user= sonuc.scalars().first()# GÜVENLİK İPUCU: Kullanıcı yoksa bile "E-posta kayıtlı değil" demiyoruz.# Saldırganların sistemde hangi e-postaların kayıtlı olduğunu anlamasını engellemek için her durumda aynı cevabı dönüyoruz.ifuser:token= create_password_reset_token(user.email)# Gerçek senaryoda e-posta gönderilirbackground_tasks.add_task(print,f"[ŞİFRE SIFIRLAMA] Link: https://projem.com{token}")return{"mesaj":"Eğer e-posta adresi sistemimizde kayıtlıysa, şifre sıfırlama talimatları gönderilecektir."}@router.post("/password-reset-confirm")asyncdefpassword_reset_confirm(data: PasswordResetConfirmSchema,session: AsyncSession = Depends(get_session)):try:payload= jwt.decode(data.token, SecurityConfig.JWT_SECRET, algorithms=[SecurityConfig.JWT_ALGORITHM])ifpayload.get("action") !="password_reset":raiseHTTPException(status_code=400, detail="Geçersiz şifre sıfırlama işlemi.")= payload.get("sub")sorgu= select(User).where(User.email == email)sonuc=awaitsession.execute(sorgu)user= sonuc.scalars().first()ifnotuser:raiseHTTPException(status_code=404, detail="Kullanıcı bulunamadı.")# Yeni şifreyi güvenli bir şekilde hash'leyip güncelliyoruzuser.password_hash = hash_password(data.new_password)awaitsession.commit()return{"mesaj":"Şifreniz başarıyla güncellendi. Yeni şifrenizle giriş yapabilirsiniz."}exceptjwt.ExpiredSignatureError:raiseHTTPException(status_code=400, detail="Şifre sıfırlama linkinin süresi dolmuş (15 dakika).")exceptjwt.PyJWTError:raiseHTTPException(status_code=400, detail="Geçersiz şifre sıfırlama token'ı.")
Sistem mimarimizin performansını ve ölçeklenebilirliğini zirveye çıkaracak olan, ağır ve uzun süren süreçleri ana uygulama akışından koparıp kuyruklara teslim edeceğimiz Background Tasks & Asynchronous Jobs (Part 20) bölümüne geçiyoruz.
Bu bölümde, FastAPI içindeki
yerleşik BackgroundTasks
yapısı ile endüstri standardı olan dağıtık görev kuyruğu
Celery ve mesaj
aracısı (message broker) Redis
mimarisi arasındaki farkları ele alacak, tam teşekküllü bir
Celery işçisi (worker) tasarlayacak ve asenkron görev yönetimini
kodlayacağız.
Bir web isteği sırasında (Request-Response yaşam döngüsünde) kullanıcının tarayıcıda veya mobil uygulamada bekletilme süresi ideal olarak 200 milisaniyenin altında olmalıdır. Ancak büyük raporlar üretmek, PDF dosyaları oluşturmak, resimleri optimize etmek veya toplu e-postalar göndermek saniyeler, hatta dakikalar alabilir. Bu tür "ağır" iş yükleri ana sunucu iş parçacığını (thread) bloke etmemek için arka plana devredilir.
FastAPI'nin yerleşik bir
BackgroundTasks
mekanizması vardır. Ancak bu mekanizma aynı Python
sürecinde (process) çalışır.
Eğer sunucu çökerse kuyruktaki görevler kaybolur ve CPU'yu yoğun
kullanan işlerde ana uygulamayı yavaşlatabilir.
Celery ise tamamen bağımsız süreçlerde çalışan, görevleri kalıcı olarak Redis veya RabbitMQ gibi kuyruklarda saklayan ve binlerce görevi paralel işleyebilen profesyonel bir asenkron görev kuyruğudur.
[İstemci] ──(İstek atar)──> [FastAPI] ──(Görevi Tetikler)──> [Redis Kuyruğu]│[İstemci] <──(İş ID'si döner)─ [FastAPI] │ (Görevi Çeker)▼[Celery Worker] (Ağır İşi Yapar)
Öncelikle gerekli kütüphaneleri projemize ekleyelim:
pip install celery redis
src/celery_app.py
(Celery Yapılandırma Dosyası):
fromceleryimportCelery# Celery uygulamasını somutlaştırıyoruz.# broker: Görevlerin birikeceği kuyruk (Redis)# backend: Tamamlanan görevlerin sonuçlarının saklanacağı yer (Redis)celery_app= Celery("fastapi_tasks",broker="redis://localhost:6379/0",backend="redis://localhost:6379/0")# Celery ayarlarını yapılandırıyoruzcelery_app.conf.update(task_serializer="json",result_serializer="json",accept_content=["json"],timezone="Europe/Istanbul",enable_utc=True,)# Ağır işi yapacak olan örnek bir Celery görevi (Task) tanımlıyoruz@celery_app.task(name="tasks.agir_rapor_olustur_task")defagir_rapor_olustur_task(kullanici_email: str,rapor_tipi: str):"""Bu fonksiyon FastAPI sürecinin tamamen DIŞINDA,bağımsız bir Celery Worker sürecinde çalışacaktır."""importtimeprint(f"[Celery]{kullanici_email}için{rapor_tipi}raporu hazırlanmaya başlandı...")# 10 saniye süren ağır bir veritabanı veya dosya işlemi simülasyonutime.sleep(10)print(f"[Celery] Rapor hazırlandı ve{kullanici_email}adresine mail atıldı.")return{"durum":"Başarılı","rapor_tipi": rapor_tipi,"sure_sn":10}
src/routers/raporlar.py
(Rapor Tetikleme ve Durum Kontrol API'ları):
fromfastapiimportAPIRouter,statusfromcelery.resultimportAsyncResultfromcelery_appimportagir_rapor_olustur_taskrouter= APIRouter(prefix="/raporlar", tags=["Arka Plan Görevleri (Celery)"])@router.post("/tetikle", status_code=status.HTTP_202_ACCEPTED)asyncdefrapor_uretimini_baslat(: str,tip: str):# Görevi senkron çağırmıyoruz ( agir_rapor_olustur_task(email, tip) ŞEKLİNDE DEĞİL! )# .delay() kullanarak görevi anında Redis kuyruğuna fırlatıyoruz.gorev= agir_rapor_olustur_task.delay(email, tip)# İstemciye 10 saniye beklemeden ANINDA 202 (Kabul Edildi) durum koduyla iş id'sini dönüyoruz.return{"mesaj":"Rapor üretim görevi arka plana atıldı.","gorev_id": gorev.id,"durum_kontrol_adresi":f"/raporlar/durum/{gorev.id}"}@router.get("/durum/{gorev_id}")asyncdefrapor_durumu_sorgula(gorev_id: str):# Redis backend üzerinden görevin güncel durumunu sorguluyoruzgorev_sonucu= AsyncResult(gorev_id)return{"gorev_id": gorev_id,"durum": gorev_sonucu.state,# PENDING, STARTED, SUCCESS, FAILURE"sonuc": gorev_sonucu.resultifgorev_sonucu.ready()elseNone}
Bu mimarinin çalışabilmesi için terminalinizde 3 ayrı sürecin eş zamanlı açık olması gerekir:
# 1. Terminal: Redis Sunucusunun Başlatılması (Eğer bilgisayarınızda kuruluysa)redis-server# 2. Terminal: FastAPI Sunucusunun Başlatılmasıuvicorn main:app --reload# 3. Terminal: Celery İşçisinin (Worker) Başlatılması (Proje kök dizininde)celery -A celery_app.celery_app worker --loglevel=info
Artık /raporlar/tetikle
uç noktasına bir istek attığınızda, FastAPI sunucunuz
milisaniyeler içinde yanıt verecek, Celery Worker terminalinizde
ise 10 saniye boyunca işlemin yapıldığını ve başarıyla
tamamlandığını gözlemleyebileceksiniz.
API'larımızın kalitesini, dökümantasyon standartlarını ve güvenilirliğini en üst seviyeye çıkaracağımız Documentation & Testing (Part 21 ve Part 22) bölümüne geçiyoruz.
Bu bölümde, FastAPI'nin sunduğu otomatik dökümantasyon araçlarını (Swagger UI ve Redoc) profesyonelce özelleştirecek, ardından projenin asenkron katmanlarını Pytest kullanarak test edecek, dış bağımlılıkları (veritabanı, dış API'lar) Unittest Mock ile izole edecek ve son olarak mülkiyet tabanlı (property-based) test aracı Schemathesis ile API'ımızı otomatik olarak hatalara karşı tarayacağız.
Bir API'ın başarısı, geliştiriciler tarafından ne kadar kolay anlaşıldığı (Dökümantasyon) ve kod değişikliklerinde sistemin kırılıp kırılmadığı (Test Otomasyonu) ile doğrudan ilişkilidir.
FastAPI, OpenAPI standartlarını kullanarak dökümantasyonu otomatik üretir. Ancak kurumsal bir görünüm için başlıkları, açıklamaları, sürümleri ve güvenlik şemalarını özelleştirmemiz gerekir.
src/main.py
(Gelişmiş OpenAPI Ayarları):
fromfastapiimportFastAPIfromfastapi.openapi.utilsimportget_openapiapp= FastAPI(title="Akademi REST API Portalı",description="JWT, SQLModel ve Celery entegrasyonuna sahip, yüksek performanslı asenkron eğitim yönetim API dökümantasyonu.",version="1.4.2",docs_url="/gelistirici-docs",# Varsayılan /docs adresini güvenlik için değiştiriyoruzredoc_url="/mimari-docs"# Varsayılan /redoc adresini değiştiriyoruz)defozel_openapi_semasi():# Şema zaten bir kere üretilmişse tekrar hesaplamamak için önbellekten (cache) dönerifapp.openapi_schema:returnapp.openapi_schemaopenapi_schema= get_openapi(title=app.title,version=app.version,description=app.description,routes=app.routes,)# Şemaya kurumsal logo veya ekstra meta veriler eklemeopenapi_schema["info"]["x-logo"] = {"url":"https://firmamiz.com"}app.openapi_schema = openapi_schemareturnapp.openapi_schema# FastAPI'nin varsayılan şema üreticisini kendi fonksiyonumuzla değiştiriyoruzapp.openapi = ozel_openapi_semasi
Asenkron FastAPI projelerinde test yazarken, gerçek veritabanını kirletmemek için bellek içi (In-Memory) bir test veritabanı kurmak veya dış servis çağrılarını taklit etmek (Mocking) gerekir.
Gerekli test kütüphanelerini kuralım:
pip install pytest pytest-asyncio httpx
src/tests/conftest.py
(Test Bağımlılıkları ve Yapılandırma Dosyası):
importpytestfromhttpximportAsyncClientfromsqlmodelimportSQLModel,create_enginefromsqlmodel.poolimportStaticPoolfromsrc.mainimportappfromsrc.dbimportget_session# Testler için bellekte (In-Memory) çalışacak sahte bir SQLite veritabanı motoru oluşturuyoruzTEST_DATABASE_URL="sqlite:///:memory:"@pytest.fixture(name="test_session")deftest_session_fixture():engine= create_engine(TEST_DATABASE_URL,connect_args={"check_same_thread":False},poolclass=StaticPool)SQLModel.metadata.create_all(engine)withSession(engine)assession:yieldsessionSQLModel.metadata.drop_all(engine)@pytest.fixture(name="client")defclient_fixture(test_session):# Ana uygulamadaki gerçek 'get_session' bağımlılığını, test veritabanı oturumumuzla değiştiriyoruzdefoverride_get_session():yieldtest_sessionapp.dependency_overrides[get_session] = override_get_session# Testler için senkron/asenkron istek atabilecek HTTP istemcisini teslim ediyoruzwithTestClient(app)asc:yieldc# Test bitince override işlemini temizliyoruzapp.dependency_overrides.clear()
src/tests/test_kurslar.py
(Kurs Ekleme ve Sahte Servis Testi):
fromunittest.mockimportpatchdeftest_yeni_kurs_ekleme_basarili(client):# Girdi verimizi hazırlıyoruzkurs_verisi= {"baslik":"Sıfırdan İleri Seviye Go Dili","fiyat":149.99,"egitmen_id":1}# HTTP POST isteği atıyoruzresponse= client.post("/kurslar/", json=kurs_verisi)# Doğrulama (Assertion) adımlarıassertresponse.status_code ==201data= response.json()assertdata["baslik"] =="Sıfırdan İleri Seviye Go Dili"assert"id"indatadeftest_dis_servis_entegrasyonu_ile_kurs_fiyati(client):"""Dışarıdaki bir döviz kuru API'sına bağımlı olan bir endpoint'i,dış API'ya gerçekten istek atmadan Mock (taklit) kullanarak test ediyoruz."""# 'src.routers.kurslar.doviz_kuru_getir' fonksiyonunun her çağrıldığında sabit 34.5 dönmesini sağlıyoruzwithpatch("src.routers.kurslar.doviz_kuru_getir")asmock_doviz:mock_doviz.return_value =34.5response= client.get("/kurslar/fiyat-tl/1")assertresponse.status_code ==200# Mock sayesinde dış API çökmüş olsa bile testimiz kararlı çalışır
Schemathesis, API dökümantasyonunuzu (OpenAPI şemasını) okur ve uç noktalarınıza binlerce kombinasyonda rastgele, hatalı, sınırları zorlayan (Örn: metin beklenen yere sayı, aşırı uzun dizgiler) veriler göndererek sisteminizi çökertmeye çalışır. API'ınızın beklenmedik durumlarda 500 (Internal Server Error) hatası vermesini engellemek için mükemmel bir araçtır.
Kurulum ve Çalıştırma:
pip install schemathesis# API sunucunuz uvicorn ile localhost:8000 üzerinde çalışırken şu komutu tetikleyin:schemathesis run http://localhost:8000/openapi.json
Schemathesis, tüm API yollarınızı otomatik olarak bulur ve veri doğrulama açıklarınızı tarayıp raporlar.
Geliştirdiğimiz tüm bu katmanları (FastAPI, PostgreSQL, Redis ve Celery Worker) bir araya getirip production ortamına, yani canlı sunuculara taşımaya hazır hale getireceğimiz Final Testing & Deployment (Part 23) bölümüne geçiyoruz.
Bu bölümde, projenin canlı ortam ayarlarını yönetmek için Gelişmiş Çevre Değişkenleri (Environment Variables) mimarisini kuracak, üretim ortamında (production) güvenliği ve performansı optimize etmek için gereken Uvicorn/Gunicorn yapılandırmasını yapacak ve son olarak tüm bu ekosistemi tek bir komutla ayağa kaldırabileceğimiz endüstri standardı Docker & Docker Compose mimarisini tasarlayacağız.
Bir uygulamayı production ortamına alırken geliştirme ortamından (development) farklı olarak en çok dikkat edilmesi gereken unsurlar şunlardır:
Gizlilik ve
Güvenlik: Şifreler, veritabanı adresleri ve gizli anahtarlar
asla kaynak kodun içinde (main.py
veya models.py)
düz metin olarak yer almamalıdır. Çevre değişkenleri (.env)
kullanılmalıdır.
Süreç Yönetimi (Process
Management): uvicorn
main:app --reload komutu geliştirme amaçlıdır.
Canlı ortamda çoklu iş parçacığı (multi-worker) yönetimi
yapabilen, sunucu çöktüğünde uygulamayı otomatik yeniden
başlatan yapılar kurulmalıdır.
Canlı ortam yapılandırmalarını
yönetmek için en kararlı yol pydantic-settings
kütüphanesini kullanmaktır.
pip install pydantic-settings
src/config.py
(Production Ayar Yönetimi):
frompydantic_settingsimportBaseSettings,SettingsConfigDictclassSettings(BaseSettings):# Varsayılan değerler atanır, ancak .env dosyasında ne yazıyorsa o geçerli olurPROJECT_NAME: str ="Akademi API Production"SECRET_KEY: strALGORITHM: str ="HS256"# Veritabanı ve Redis bağlantı adresleriDATABASE_URL: strREDIS_URL: str ="redis://redis:6379/0"# .env dosyasını otomatik okuması için Pydantic yapılandırmasımodel_config= SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")# Tek bir settings nesnesi üzerinden tüm uygulamada ayarlara erişeceğizsettings= Settings()
Canlı Ortam İçin Kök Dizinde Bulunacak .env
Dosyası:
SECRET_KEY=9a8b7c6d5e4f3210zyxwvutsrqponmlk_canli_anahtarDATABASE_URL=postgresql+asyncpg://db_user:guvenli_sifre@postgres_db:5432/canli_akademi_dbREDIS_URL=redis://redis_service:6379/0
Tüm sistem bileşenlerinin (FastAPI, Celery, Postgres, Redis) farklı sunucularda veya aynı sunucu içinde birbirini etkilemeden izole çalışabilmesi için Docker imajları hazırlarız.
Proje Kök Dizini İçin Dockerfile:
# 1. Aşama: Python taban imajını seçiyoruzFROMpython:3.11-slim# 2. Aşama: Çalışma dizinini ayarlıyoruzWORKDIR/app# 3. Aşama: Python'un logları anlık ekrana basmasını sağlıyoruzENVPYTHONUNBUFFERED=1# 4. Aşama: Bağımlılıkları kopyalayıp yüklüyoruzCOPYrequirements.txt .RUNpip install --no-cache-dir -r requirements.txt# 5. Aşama: Tüm proje kodlarını konteyner içine kopyalıyoruzCOPY. .# 6. Aşama: Varsayılan olarak API'ı ayağa kaldıracak komut (Celery için bu geçersiz kılınacak)CMD["gunicorn","src.main:app","-w","4","-k","uvicorn.workers.UvicornWorker","-b","0.0.0.0:8000"]
Mimarinin Açıklaması:
Canlı ortamda gunicorn
orkestrasyon aracını öne koyup arkasına 4 adet asenkron uvicorn
işçisi (UvicornWorker)
bağlıyoruz. Bu sayede sunucu, gelen binlerce eş zamanlı isteği
(concurrent requests) işlemcideki çekirdeklere dağıtarak bloke
olmadan yanıtlayabilir.
Tüm mikro servis bileşenlerini
tek bir orkestrasyon dosyasıyla ayağa kaldırmak için projenin kök
dizininde docker-compose.yml
dosyası tasarlarız.
docker-compose.yml:
version:'3.8'services:# 1. Servis: PostgreSQL Veritabanıpostgres_db:image:postgres:15-alpinecontainer_name: production_postgresenvironment:POSTGRES_USER: db_userPOSTGRES_PASSWORD: guvenli_sifrePOSTGRES_DB: canli_akademi_dbvolumes:-postgres_data:/var/lib/postgresql/dataports:-"5432:5432"# 2. Servis: Mesaj Aracısı ve İptal Listesi (Redis)redis_service:image:redis:7-alpinecontainer_name: production_redisports:-"6379:6379"# 3. Servis: FastAPI Web API Uygulamamızweb_api:build: .container_name: production_fastapiports:-"8000:8000"env_file:- .envdepends_on:- postgres_db- redis_service# 4. Servis: Ağır İşleri Sırtlanacak Celery İşçisicelery_worker:build: .container_name: production_celery# Dockerfile içindeki varsayılan Gunicorn komutunu ezerek Celery Worker'ı başlatıyoruzcommand: celery -A src.celery_app.celery_app worker --loglevel=infoenv_file:- .envdepends_on:- redis_service- postgres_dbvolumes:postgres_data:
Sunucunuza (AWS, DigitalOcean, Azure vb.) Docker ve Docker Compose yükledikten sonra projenizin olduğu dizinde tek bir komut çalıştırmanız yeterlidir:
docker-compose up --build -d
Açıklama:
-d
(detached) parametresi tüm bu devasa ekosistemin arka planda (arka
plan süreci olarak) sessizce çalışmasını sağlar. Artık
sunucunuzun IP adresinin 8000 portuna gelen istekler Gunicorn/Uvicorn
tarafından karşılanacak, veri tabanı Postgres'e yazılacak,
kuyruklar Redis üzerinden bağımsız Celery konteynerine
aktarılacaktır.
Kurs müfredatının son bölümünde SQL veritabanlarına güçlü bir alternatif olarak döküman tabanlı MongoDB ve onun için geliştirilmiş Pydantic uyumlu modern asenkron ODM (Object Document Mapper) kütüphanesi olan Beanie yer almaktadır. SQLModel ile yaptığımız tüm doğrulamalar ve Pydantic entegrasyonu, Beanie sayesinde MongoDB'de de birebir korunur.
pip install beanie motor
(Not: motor,
MongoDB için asenkron Python sürücüsüdür.)
src/nosql_models.py
(Beanie Modeli):
frombeanieimportDocumentfrompydanticimportField# Beanie Document sınıfından türetilen modeller doğrudan MongoDB koleksiyonu (collection) olurclassSistemLogDocument(Document):islem_adi: strkullanici: strdetay: stretki_seviyesi: int = Field(default=1)classSettings:name="sistem_loglari"# MongoDB'deki koleksiyon adı
src/main.py
dosyasına eklenecek MongoDB Lifespan Başlatıcısı:
frommotor.motor_asyncioimportAsyncIOMotorClientfrombeanieimportinit_beaniefromnosql_modelsimportSistemLogDocument# Lifespan içine eklenecek asenkron MongoDB başlatma adımı:asyncdefinit_nosql():client= AsyncIOMotorClient("mongodb://localhost:27017")# Beanie'yi bağlayıp modellerimizi MongoDB'ye tanıtıyoruzawaitinit_beanie(database=client.akademi_nosql_db, document_models=[SistemLogDocument])
Projemizi modern bulut mühendisliği standartlarına ulaştıracak, manuel müdahaleleri sıfırlayacak CI/CD (Sürekli Entegrasyon / Sürekli Dağıtım) boru hattı (pipeline) mimarisine ve canlı sistemlerin takibi için hayati önem taşıyan Docker Log İzleme operasyonlarına geçiyoruz.
Bir uygulamayı her güncellemede
sunucuya bağlanıp manuel olarak git
pull ve
docker-compose
up
yaparak canlıya almak; hem insan hatasına çok açıktır hem de
sistem kesintilerine (downtime) yol açabilir. Sektör standardı,
kod GitHub'a
pushlandığı anda testlerin otomatik çalışması, imajın
üretilmesi ve sunucuya otomatik yüklenmesidir.
Bu senaryoda, kodlarımızı
barındırdığımız GitHub platformunun yerleşik otomasyon aracı
olan GitHub Actions'ı
kullanacağız. Amacımız, ana daldaki (main
branch) her kod güncellemesinde projenin otomatik olarak bulut
sunucumuza (Örn: AWS EC2, DigitalOcean Damlacığı, Ubuntu
Sunucusu) aktarılmasıdır.
Projenizin kök dizininde
.github/workflows/deploy.yml
adında bir dosya oluşturun:
name: FastAPI Canlıya Alım (Deployment) Boru Hattı# Sadece main dalına (branch) bir kod pushlandığında çalışıron:push:branches:- mainjobs:test_ve_deploy:runs-on: ubuntu-lateststeps:# 1. Adım: GitHub üzerindeki kodu otomasyon sunucusuna çeker-name: Kodu Kopyala (Checkout)uses: actions/checkout@v4# 2. Adım: Python ortamını hazırlar ve Pytest testlerini koşturur-name: Python Ortamı Kur ve Test Etuses: actions/setup-python@v5with:python-node:'3.11'-name: Bağımlılıkları Yükle ve Testleri Çalıştırrun:|pip install -r requirements.txtpytest src/tests/# 3. Adım: Testler başarılıysa, SSH üzerinden bulut sunucumuza bağlanır ve komutları çalıştırır-name: Bulut Sunucusuna Bağlan ve Güncelle (Deploy)uses: appleboy/ssh-action@v1.0.3with:host: ${{ secrets.SUNUCU_IP_ADRESI }}# Sunucunuzun IP adresiusername: ${{ secrets.SUNUCU_KULLANICI }}# Örn: ubuntu, rootkey: ${{ secrets.SUNUCU_SSH_KEY }}# Sunucuya erişim sağlayan Private SSH Keyscript:|cd /var/www/fastapi-akademigit pull origin main# Sadece değişen servisleri arka planda sessizce yeniden inşa edip kaldırırdocker-compose up --build -d web_api celery_worker
Güvenlik Notu:
Yukarıdaki secrets.
ile başlayan alanlar kesinlikle koda açık yazılmaz. GitHub
projenizin ayarlarından Settings -> Secrets and
variables -> Actions kısmına
gidilerek güvenli bir şekilde sunucu bilgileri olarak tanımlanır.
Uygulama canlıya alındıktan sonra arka planda ne olduğunu anlamanın, hataları (500 hataları, veritabanı kopmaları, Celery çöküşleri) yakalamanın tek yolu Docker loglarını doğru filtrelemektir. Sunucu terminaline SSH ile bağlandıktan sonra şu pratik komut şablonlarını kullanırız:
Herhangi bir servisin loglarını,
sanki bir video akışı gibi canlı olarak ekrandan izlemek için -f
(follow) parametresi kullanılır.
# Sadece FastAPI web sunucusunun loglarını canlı takip etmedocker logs -f production_fastapi# Celery işçisinin arka planda hangi görevleri yaptığını anlık izlemedocker logs -f production_celery
Eğer konteyner aylardır
çalışıyorsa, doğrudan docker
logs
demek ekrana milyonlarca satır basabilir ve terminali
kilitleyebilir. --tail
parametresi ile sadece son gelişmeleri okuruz.
# FastAPI'nin en son ürettiği 100 log satırını getirdocker logs --tail100production_fastapi
Kod loglarınızın içinde tarih bilgisi yazmıyorsa, Docker'ın kendi zaman damgalarını ekrana yazdırmasını sağlayabilirsiniz.
docker logs -t --tail 50 production_fastapi
Log yığınının içinden sadece
"Hata" veya "Kritik" durumları ayıklamak için
Linux'un güçlü aracı grep
ile Docker loglarını birleştiririz.
# FastAPI logları içinde sadece HTTP 500 (Internal Server Error) veren satırları filtreledocker logs production_fastapi2>&1| grep"500"# Celery logları içinde hataya (ERROR / FAILURE) neden olan görevleri bulmadocker logs production_celery2>&1| grep -i"error"
(Not: 2>&1
ifadesi, Docker'ın standart hata çıktılarını (stderr) standart
çıktılara (stdout) yönlendirerek grep
komutunun hatasız arama yapmasını sağlar.)