Görseldeki ders müfredatını ve FastAPI eğitim içeriğini adım adım analiz ederek, sizden gelen tüm kurallar doğrultusunda (süreleri, "Önizleme" ve "ders" satırlarını, anlamsız ifadeleri temizleyerek) detaylı bir konu anlatım rehberi ve uygulama kılavuzu haline getirdim.

Yarıda kalmamak ve her konuyu derinlemesine, bol kod örneğiyle açıklayabilmek adına bu uzun müfredatı parçalara ayırarak ilerliyoruz. İlk olarak "Öğrenecekleriniz" bölümünün kapsamlı analizi ve ardından ilk ana konuların detaylı anlatımıyla başlıyoruz.


🎯 BÖLÜM 1: "Öğrenecekleriniz" Paragrafının Açıklaması

Bu eğitim içeriği, modern web dünyasının en popüler Python framework'lerinden biri olan FastAPI ile sıfırdan ileri seviyeye güvenli bir REST API geliştirmeyi hedefler. Bu bölümde vaat edilen yetkinliklerin teknik karşılıkları şunlardır:


🛠️ BÖLÜM 2: GİRİŞ VE TEMEL KAVRAMLAR

API Nedir?

API (Application Programming Interface), iki yazılımın birbiriyle konuşmasını sağlayan bir köprüdür. REST API ise web üzerinde HTTP protokolünün metotlarını (GET, POST, PUT, DELETE) kullanarak veri alışverişi yapan standart bir mimaridir.

FastAPI Nedir ve Avantajları Nelerdir?

FastAPI; Python 3.8+ ile standart Python tip ipuçlarına dayanan, modern ve çok yüksek performanslı bir web framework'üdür. En büyük avantajları hızı (NodeJS ve Go ile yarışır seviyede), otomatik dökümantasyon üretmesi ve hataları otomatik yakalamasıdır.


🔥 UYGULAMA 1: İlk FastAPI Uygulamasını Oluşturma

İlk olarak sisteminizde FastAPI ve sunucu olarak çalışabilmesi için Uvicorn'un kurulu olması gerekir.

pip install fastapi uvicorn

Şimdi en basit haliyle bir API oluşturalım. Bir main.py dosyası oluşturun:

from fastapi import FastAPI

# FastAPI nesnesini başlatıyoruz
app = FastAPI(
    title="Benim İlk Başarılı API'm",
    description="FastAPI müfredat eğitimi kapsamında oluşturulmuş ilk projedir.",
    version="1.0.0"
)

# Kök (Root) dizine bir GET isteği tanımlıyoruz
@app.get("/")
def ana_sayfa():
    return {"mesaj": "FastAPI dünyasına hoş geldiniz!"}

Sunucuyu Başlatma:

Terminalden şu komutla API'nizi ayağa kaldırın:

uvicorn main:app --reload

--reload parametresi, kodda bir değişiklik yaptığınızda sunucunun otomatik olarak yeniden başlamasını sağlar.


🗺️ BÖLÜM 3: PARAMETRELER VE OTOMATİK DÖKÜMANTASYON

API'lara dışarıdan veri aktarmanın en temel iki yolu Path (Yol) ve Query (Sorgu) parametreleridir.

1. Path Parameters (Yol Parametreleri)

URL yapısının ayrılmaz bir parçası olan dinamik değişkenlerdir. Genellikle doğrudan belirli bir kaynağı (ID ile) işaret etmek için kullanılır.

2. Query Parameters (Sorgu Parametreleri)

URL'in sonuna ? işaretinden sonra eklenen anahtar-değer çiftleridir. Genellikle filtreleme, sıralama veya sayfalama (pagination) işlemlerinde tercih edilir.


🔥 UYGULAMA 2: Eşzamanlı Yol ve Sorgu Parametresi Kullanımı

Aşağıdaki örnekte hem veri tipini zorunlu kılmayı, hem otomatik dökümantasyonu incelemeyi, hem de isteğe bağlı (optional) ve zorunlu (required) parametre mantığını göreceğiz.

from fastapi import FastAPI
from typing import Optional

app = FastAPI()

# Sahte bir ürün veritabanı simüle edelim
URUNLER = {
    1: {"isim": "Kablosuz Kulaklık", "fiyat": 1200, "kategori": "Elektronik"},
    2: {"isim": "Akıllı Saat", "fiyat": 3500, "kategori": "Elektronik"},
    3: {"isim": "Koşu Ayakkabısı", "fiyat": 1800, "kategori": "Spor"}
}

# 1. Path Parameter ve Tip Kontrolü
# '{urun_id}' bir yol parametresidir ve ': int' diyerek tam sayı olması zorunlu tutulmuştur.
@app.get("/urunler/{urun_id}")
def urun_detay_getir(urun_id: int):
    if urun_id in URUNLER:
        return URUNLER[urun_id]
    return {"hata": "Ürün bulunamadı"}

# 2. Path & Query Parametrelerinin Birlikte Kullanımı
# 'kategori' sorgu parametresi opsiyoneldir, 'limit' ise varsayılan değer olarak 10 almıştır.
@app.get("/urun-ara/")
def urun_filtrele(kategori: Optional[str] = None, limit: int = 10):
    sonuclar = list(URUNLER.values())
    
    if kategori:
        sonuclar = [u for u in sonuclar if u["kategori"].lower() == kategori.lower()]
        
    return sonuclar[:limit]

📝 Otomatik Dökümantasyon (Swagger UI)

FastAPI'nin en büyüleyici özelliklerinden biri olan otomatik dökümantasyonu görmek için tarayıcınızdan şu adrese gidin:
👉
http://127.0.0

Bu arayüz sayesinde yazdığınız tüm fonksiyonlar, beklenen parametre tipleri interaktif bir dökümantasyona dönüşür. Ekstra hiçbir şey yazmadan API'nizi buradan "Try it out" butonuna basarak test edebilirsiniz.


📦 BÖLÜM 4: REQUEST BODY (İSTEK GÖVDESİ) VE PYDANTIC MODELLERİ

Bir API'ye yeni bir veri kaydetmek istediğimizde (Örn: Yeni kullanıcı kaydı, yeni ürün ekleme), bu veriyi URL üzerinden göndermek güvenli ve pratik değildir. Veriyi HTTP Request Body (İstek Gövdesi) içinde JSON formatında göndeririz.

FastAPI, bu gelen karmaşık JSON verilerini doğrulamak ve Python nesnelerine dönüştürmek için Pydantic kütüphanesini kullanır.


🔥 UYGULAMA 3: Gelişmiş Pydantic Modelleme ve POST İsteği

Bu uygulamada iç içe geçmiş (nested) veri tiplerini, metadata tanımlamayı ve örnek veri (example data) sağlamayı göreceğiz.

from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime

app = FastAPI()

# 1. Alt Model Tanımlama (Nesting Pydantic Models)
class Ozellik(BaseModel):
    renk: str
    boyut: str

# 2. Ana Model Tanımlama (Request Body için)
class UrunEklemeModeli(BaseModel):
    # Field kullanarak kısıtlamalar ve açıklamalar (metadata) ekliyoruz
    isim: str = Field(..., title="Ürün Adı", min_length=2, max_length=50)
    fiyat: float = Field(..., gt=0, description="Ürün fiyatı sıfırdan büyük olmalıdır.")
    etiketler: List[str] = [] # Python veri tipi iç içe kullanımı
    detaylar: Optional[Ozellik] = None # Başka bir Pydantic modelini iç içe ekleme
    eklenme_tarihi: datetime = Field(default_factory=datetime.now) # Zaman veri tipi kullanımı

    # Swagger dökümantasyonunda gözükecek örnek şablon verisi
    class Config:
        json_schema_extra = {
            "example": {
                "isim": "Oyuncu Faresi",
                "fiyat": 750.50,
                "etiketler": ["elektronik", "gaming", "rgb"],
                "detaylar": {
                    "renk": "Siyah",
                    "boyut": "Standart"
                }
            }
        }

# POST metodu ile yeni veri kabul eden API rotası
@app.post("/urunler/", status_code=201)
def yeni_urun_ekle(urun: UrunEklemeModeli):
    # Gelen veri Pydantic sayesinde otomatik olarak doğrulanmıştır.
    # urun.isim veya urun.fiyat şeklinde doğrudan erişebiliriz.
    
    urun_sozluk = urun.model_dump()
    # Gerçek senaryoda burası veritabanına kaydedilir.
    
    return {
        "mesaj": "Ürün başarıyla doğrulandı ve (sanal olarak) eklendi.",
        "eklenen_urun": urun_sozluk
    }

Eğer bu endpoint'e fiyat: -5 veya isim: "A" (minimum karakter sınırının altında) şeklinde hatalı bir veri göndermeye çalışırsanız, FastAPI sizin yerinize isteği reddeder ve istemciye hatanın tam olarak nerede olduğunu söyleyen temiz bir 422 Unprocessable Entity yanıtı döner.




Harika! Tam hızla, müfredatın en kritik ve omurgasını oluşturan veritabanı, CRUD işlemleri ve ilişkisel modelleme bölümlerine geçiyoruz. Temizlik kurallarımızı (süreler, önizleme ve ders ibarelerini kaldırma) koruyarak tamamen üretime yönelik, çalışan kodlar üzerinden ilerleyelim.


🗄️ BÖLÜM 5: VERİTABANI BAĞLANTISI (SQLAlchemy & SQLite)

Gerçek dünya uygulamalarında veriler bellekte (RAM) tutulmaz; kalıcı olmaları için veritabanına yazılır. Bu bölümde Python dünyasının en güçlü ORM (Object-Relational Mapping) kütüphanelerinden biri olan SQLAlchemy kullanarak veritabanı mimarisini kuracağız. ORM sayesinde SQL kodu yazmadan, Python sınıfları ile tablolar oluşturup veri yöneteceğiz.

Projemizde dosya tabanlı, kurulum gerektirmeyen hafif bir veritabanı olan SQLite kullanacağız.


🔥 UYGULAMA 4: Proje Yapısının Kurulması ve Veritabanı Bağlantısı

Büyük projeleri modüler hale getirmek eğitimin ana amaçlarından biridir. Bu yüzden kodumuzu parçalara ayırarak tasarlıyoruz.

1. database.py - Bağlantı Ayarları

Bu dosyada veritabanı motorunu (engine) oluşturacak ve her istekte açılıp kapanacak veritabanı oturumlarını (SessionLocal) tanımlayacağız.

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# SQLite veritabanı dosyasının yolu
SQLALCHEMY_DATABASE_URL = "sqlite:///./fastapi_kursu.db"

# Engine (Motor) oluşturuluyor. 
# connect_args={"check_same_thread": False} sadece SQLite için çoklu thread güvenliği sağlamak adına gereklidir.
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

# Veritabanı ile konuşacak oturum fabrikası
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Modellerimizin miras alacağı ana sınıf
Base = declarative_base()

# Bağımlılık Enjeksiyonu (Dependency Injection) için fonksiyon
# Bu fonksiyon her API isteğinde bir veritabanı oturumu açar ve iş bitince kapatır.
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

2. models.py - Veritabanı Tablosunun Tanımlanması

SQLAlchemy kullanarak veritabanındaki tablolarımızın şemasını Python sınıfı olarak tasarlıyoruz.

from sqlalchemy import Column, Integer, String, Float, Boolean
from database import Base

class ProductModel(Base):
    __tablename__ = "products" # Veritabanındaki tablo adı

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False)
    price = Column(Float, nullable=False)
    is_active = Column(Boolean, default=True)

🔄 BÖLÜM 6: CRUD OPERASYONLARININ GERÇEKLEŞTİRİLMESİ

CRUD (Create, Read, Update, Delete), veritabanı işlemlerinin temelidir. Bu bölümde hem veritabanı işlemlerini yapacak endpoint'leri yazacağız hem de Response Model (Yanıt Modeli) ile dışarıya döneceğimiz veriyi filtreleyeceğiz. Ayrıca hata yönetimini (HTTPException) entegre edeceğiz.


🔥 UYGULAMA 5: Tam Kapsamlı Ürün CRUD API'si

Şimdi schemas.py dosyasında Pydantic modellerimizi (istek ve yanıt şemaları) tanımlayalım, ardından main.py içinde tüm operasyonları birleştirelim.

schemas.py - Pydantic İstek ve Yanıt Şemaları

from pydantic import BaseModel

# Kullanıcıdan ürün eklerken/güncellerken beklediğimiz şema
class ProductBase(BaseModel):
    name: str
    price: float
    is_active: bool = True

class ProductCreate(ProductBase):
    pass # Ekstra alan gerekirse buraya eklenir

# API'den istemciye döneceğimiz şema (Response Model)
# Veritabanındaki 'id' alanını da içermelidir.
class ProductResponse(ProductBase):
    id: int

    class Config:
        from_attributes = True # SQLAlchemy nesnesini Pydantic şemasına dönüştürmeyi sağlar (Eski adıyla orm_mode)

main.py - CRUD Endpoint'leri

from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
import models
import schemas
from database import engine, get_db

# Veritabanı tablolarını otomatik oluşturuyoruz (Uygulama başlarken)
models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="Ürün Yönetim API'si")

# 1. CREATE (Veri Ekleme)
# response_model kullanarak başarılı yanıtta sadece 'ProductResponse' alanlarının döneceğini garanti ediyoruz.
@app.post("/products/", response_model=schemas.ProductResponse, status_code=status.HTTP_201_CREATED)
def create_product(product: schemas.ProductCreate, db: Session = Depends(get_db)):
    new_product = models.ProductModel(
        name=product.name,
        price=product.price,
        is_active=product.is_active
    )
    db.add(new_product)
    db.commit()
    db.refresh(new_product) # Oluşan ID değerini almak için yeniliyoruz
    return new_product

# 2. READ ALL (Tüm Ürünleri Listeleme)
@app.get("/products/", response_model=list[schemas.ProductResponse])
def get_products(db: Session = Depends(get_db)):
    products = db.query(models.ProductModel).all()
    return products

# 3. READ ONE (Tek Ürün Getirme & İstisna Yönetme)
@app.get("/products/{product_id}", response_model=schemas.ProductResponse)
def get_product(product_id: int, db: Session = Depends(get_db)):
    product = db.query(models.ProductModel).filter(models.ProductModel.id == product_id).first()
    if not product:
        # Eğer ürün bulunamazsa HTTP 404 hatası fırlatıyoruz
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Aradığınız ürün bulunamadı!")
    return product

# 4. UPDATE (Ürün Güncelleme)
@app.put("/products/{product_id}", response_model=schemas.ProductResponse)
def update_product(product_id: int, updated_data: schemas.ProductCreate, db: Session = Depends(get_db)):
    product_query = db.query(models.ProductModel).filter(models.ProductModel.id == product_id)
    product = product_query.first()
    
    if not product:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Güncellenmek istenen ürün bulunamadı!")
        
    product_query.update(updated_data.model_dump(), synchronize_session=False)
    db.commit()
    return product_query.first()

# 5. DELETE (Ürün Silme)
@app.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_product(product_id: int, db: Session = Depends(get_db)):
    product_query = db.query(models.ProductModel).filter(models.ProductModel.id == product_id)
    product = product_query.first()
    
    if not product:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Silinmek istenen ürün bulunamadı!")
        
    product_query.delete(synchronize_session=False)
    db.commit()
    # 204 No Content dönüldüğü için gövdede bir veri döndürmüyoruz.
    return None

🔗 BÖLÜM 7: ÇOKLU MODELLER, İLİŞKİLER VE APIROUTER

Gerçek senaryolarda tablolar birbirine bağlıdır. Örneğin, projemize bir de Seller (Satıcı) modeli ekleyelim. Bir satıcının birden fazla ürünü olabilir (Bire-Çok İlişki / One-to-Many).

Ayrıca projedeki kod karmaşasını çözmek için rotalarımızı modüllere ayırarak APIRouter mimarisini devreye alacağız.


🔥 UYGULAMA 6: İlişkisel Veritabanı ve APIRouter Yapısı

1. models.py Güncellemesi (İlişki Kurma)

from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from database import Base

class SellerModel(Base):
    __tablename__ = "sellers"

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True, nullable=False)
    hashed_password = Column(String, nullable=False) # Şifreleri hash'lenmiş tutacağız

    # İlişki tanımı: Bir satıcının birden fazla ürünü olabilir (back_populates)
    products = relationship("ProductModel", back_populates="owner")

class ProductModel(Base):
    __tablename__ = "products"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False)
    price = Column(Float, nullable=False)
    is_active = Column(Boolean, default=True)
    
    # Yabancı Anahtar (Foreign Key) ile satıcıya bağlanma
    seller_id = Column(Integer, ForeignKey("sellers.id"))
    
    # İlişki tanımı
    owner = relationship("SellerModel", back_populates="products")

2. Modüler Rotalar: routers/seller.py

Proje kökünde routers adında bir klasör açıp içine seller.py yazarak satıcı işlemlerini APIRouter ile ana dosyadan yalıtıyoruz. Rotalarımıza prefix (önek) ve tag (etiket) ekliyoruz.

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from database import get_db
import models, schemas

# APIRouter nesnesi tanımlanıyor. Tags ve Prefix dökümantasyonu düzenler.
router = APIRouter(
    prefix="/sellers",
    tags=["Satıcı İşlemleri"]
)

# Şifre hash'leme simülasyonu (Gerçek hash'leme Authentication bölümünde yapılacak)
def fake_hash_password(password: str):
    return password + "notsecurehash"

@router.post("/", response_model=schemas.SellerResponse, status_code=201)
def create_seller(seller: schemas.SellerCreate, db: Session = Depends(get_db)):
    # Kullanıcı adı kontrolü
    db_seller = db.query(models.SellerModel).filter(models.SellerModel.username == seller.username).first()
    if db_seller:
        raise HTTPException(status_code=400, detail="Bu kullanıcı adı zaten alınmış!")
        
    hashed_pwd = fake_hash_password(seller.password)
    new_seller = models.SellerModel(username=seller.username, hashed_password=hashed_pwd)
    
    db.add(new_seller)
    db.commit()
    db.refresh(new_seller)
    return new_seller

(Not: schemas.SellerResponse ve schemas.SellerCreate şemalarının schemas.py içinde tanımlandığı varsayılmıştır).

3. Ana Dosyada Router'ı Tanımlama (main.py)

Ana main.py dosyamızı tertemiz hale getirip yazdığımız router'ı içeri dahil ediyoruz:

from fastapi import FastAPI
from database import engine
import models
from routers import seller # Yazdığımız router modülü

models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="Gelişmiş İlişkisel API")

# Router'ı uygulamaya dahil ediyoruz (Include Router)
app.include_router(seller.router)

@app.get("/")
def root():
    return {"mesaj": "Modüler API Sistemine Hoş Geldiniz"}



Harika! Müfredatın en kritik zirve noktası olan Authentication (Kimlik Doğrulama) sistemini tasarlıyoruz. Bu bölümde şifreleri güvenli bir şekilde hash'lemeyi, kullanıcı giriş yaptığında JWT (JSON Web Token) üretmeyi, bu token'ı doğrulamayı ve belirli API rotalarını sadece yetkili kullanıcıların erişimine açmayı (Rotaları Korumayı) uçtan uca uygulayacağız.

Süreleri, dökümantasyon veya gereksiz ibareleri temizleyerek tamamen üretime hazır, modern ve güvenli bir mimari üzerinden ilerleyelim.


🔐 BÖLÜM 8: AUTHENTICATION (KİMLİK DOĞRULAMA) VE JWT MİMARİSİ

JWT (JSON Web Token) Nedir ve Nasıl Çalışır?

JWT, taraflar arasında güvenli bir şekilde JSON nesnesi olarak veri taşınmasını sağlayan açık bir standarttır (RFC 7519).

  1. Kullanıcı kullanıcı adı ve şifresiyle giriş isteği (POST) yapar.

  2. Sunucu şifreyi doğrularsa, kullanıcının kimlik bilgisini (örn: kullanıcı adı, id) içeren, sunucudaki gizli bir anahtar (SECRET_KEY) ile imzalanmış şifreli bir Token (Token metni) üretir ve istemciye döner.

  3. İstemci (Frontend/Mobil), sonraki tüm isteklerinde bu token'ı HTTP başlığına (Header) Authorization: Bearer <TOKEN> şeklinde ekler.

  4. Sunucu her istekte veritabanına gitmek yerine sadece bu imzanın geçerli olup olmadığını kontrol eder. Geçerliyse isteğe izin verir.


🔥 UYGULAMA 7: Güvenlik Paketi ve Araçlarının Kurulması

Öncelikle şifre hash'leme ve JWT operasyonları için gerekli kütüphaneleri terminalden kurmamız gerekir:

pip install "passlib[bcrypt]" PyJWT python-multipart

1. security.py - Şifreleme ve Token Yardımcı Fonksiyonları

Şifre yönetimi ve JWT üretim kodlarını tek bir güvenli modülde topluyoruz.

from datetime import datetime, timedelta, timezone
import jwt
from passlib.context import CryptContext
from typing import Union

# Şifreleme algoritması ayarları (BCRYPT)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# JWT Güvenlik Ayarları (Gerçek projelerde bunları .env dosyasından okumalısınız)
SECRET_KEY = "SUPER_GIZLI_VE_GUVENLI_ANAHTAR_KODUNUZ_BURAYA"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Üretilen token'ın geçerlilik süresi (30 Dakika)

# 1. Şifreyi Hash'leme Fonksiyonu
def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

# 2. Girilen Şifre ile Hash'li Şifreyi Karşılaştırma Fonksiyonu
def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

# 3. JWT Token Üretme Fonksiyonu (Utility Function To Create JWT Token)
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str:
    to_encode = data.copy()
    
    # Süre kısıtlaması ekleme
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        
    to_encode.update({"exp": expire}) # Token içine son kullanma tarihini (expiration) ekliyoruz
    
    # Token'ı imzalayıp şifreliyoruz
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

🔥 UYGULAMA 8: Login Rotası ve Token Üretimi

Şimdi kullanıcıların kullanıcı adı ve şifreleriyle giriş yapabileceği, FastAPI'nin yerleşik OAuth2PasswordRequestForm standardına uygun giriş rotasını yazalım.

routers/auth.py - Giriş ve Yetkilendirme Rotaları

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import timedelta
import models, security
from database import get_db

router = APIRouter(tags=["Kimlik Doğrulama"])

@router.post("/token")
def login_for_access_token(
    form_data: OAuth2PasswordRequestForm = Depends(), 
    db: Session = Depends(get_db)
):
    # 1. Veritabanında kullanıcıyı ara
    user = db.query(models.SellerModel).filter(models.SellerModel.username == form_data.username).first()
    
    # 2. Kullanıcı yoksa veya şifre yanlışsa HTTP 401 Unauthorized hatası dön
    if not user or not security.verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Kullanıcı adı veya şifre hatalı!",
            headers={"WWW-Authenticate": "Bearer"},
        )
        
    # 3. Giriş başarılıysa JWT Token üret
    access_token_expires = timedelta(minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = security.create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    
    # OAuth2 standardına uygun yanıt dönüyoruz
    return {"access_token": access_token, "token_type": "bearer"}

🔥 UYGULAMA 9: Mevcut Kullanıcıyı Yakalama ve Rotaları Koruma

Bir rotayı korumak, o rotaya istek atan kullanıcının HTTP Header'ında geçerli bir JWT gönderip göndermediğini kontrol etmek demektir. FastAPI'nin Depends mekanizması ile bunu bir bağımlılık haline getireceğiz.

dependencies.py - Mevcut Kullanıcıyı Doğrulama (Get Current User)

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
from sqlalchemy.orm import Session
import security, models
from database import get_db

# Token'ın hangi URL'den alınacağını FastAPI'ye bildiriyoruz (Swagger UI entegrasyonu için)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Geçersiz veya süresi dolmuş kimlik doğrulaması!",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        # Gelen token'ı gizli anahtarımızla çözüyoruz (Decode)
        payload = jwt.decode(token, security.SECRET_KEY, algorithms=[security.ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except jwt.PyJWTError:
        raise credentials_exception
        
    # Veritabanından kullanıcıyı kontrol et
    user = db.query(models.SellerModel).filter(models.SellerModel.username == username).first()
    if user is None:
        raise credentials_exception
        
    return user # İstek yapan aktif satıcı nesnesini dönüyoruz

Korumalı Rota Örneği: routers/product.py

Aşağıdaki rotada, bir ürün eklenirken current_user: models.SellerModel = Depends(get_current_user) parametresi eklenmiştir. Bu sayede, geçerli bir JWT taşımayan hiç kimse bu rotaya erişemez ve ürün ekleyemez.

from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
import models, schemas
from database import get_db
from dependencies import get_current_user # Korumalı mekanizmamız

router = APIRouter(prefix="/products", tags=["Ürün İşlemleri"])

# KORUMALI ROTA (Protecting Routes)
@router.post("/", response_model=schemas.ProductResponse, status_code=status.HTTP_201_CREATED)
def create_product_for_seller(
    product: schemas.ProductCreate, 
    db: Session = Depends(get_db),
    current_user: models.SellerModel = Depends(get_current_user) # Giriş zorunlu!
):
    # Ürün eklenirken otomatik olarak giriş yapan satıcının id'si (seller_id) atanıyor
    new_product = models.ProductModel(
        name=product.name,
        price=product.price,
        is_active=product.is_active,
        seller_id=current_user.id # Token'dan gelen güvenli kullanıcı ID'si
    )
    db.add(new_product)
    db.commit()
    db.refresh(new_product)
    return new_product

🔥 TÜM PROJENİN BİRLEŞTİRİLMESİ (main.py)

Yazdığımız tüm bağımsız ve güvenli modülleri ana dosyamızda tek çatı altında topluyoruz:

from fastapi import FastAPI
from database import engine
import models
from routers import auth, seller, product # Tüm modüller

# Veritabanı tablolarını oluştur
models.Base.metadata.create_all(bind=engine)

app = FastAPI(
    title="Tam Teşekküllü FastAPI Kurs Projesi",
    description="OAuth2, JWT Kimlik Doğrulama, İlişkisel SQLAlchemy ORM ve APIRouter Modüler Mimari İçerir.",
    version="2.0.0"
)

# Router'ları Sırayla Dahil Ediyoruz
app.include_router(auth.router)     # /token rotası için
app.include_router(seller.router)   # /sellers rotaları için
app.include_router(product.router)  # /products rotaları için

@app.get("/")
def read_root():
    return {"durum": "Sistem Aktif", "mesaj": "Güvenli API dökümantasyonu için lütfen /docs adresini ziyaret edin."}

🚀 Test Etme Aşamaları

Uygulamayı uvicorn main:app --reload ile başlattıktan sonra http://127.0.0 adresine gittiğinizde:

  1. Sağ üst köşede bir "Authorize" kilidi göreceksiniz.

  2. Önce /sellers rotasından yeni bir satıcı hesabı oluşturun. (Bu aşamada şifreniz arka planda bcrypt ile kırılması imkansız hale getirilerek hash'lenir).

  3. Ardından "Authorize" butonuna basarak oluşturduğunuz kullanıcı adı ve şifre ile giriş yapın.

  4. Arka planda /token rotası çalışır, şifrenizi kontrol eder ve size 30 dakika geçerli bir JWT Token verir. Swagger bu token'ı tarayıcı hafızasına alır.

  5. Artık korumalı olan POST /products rotasına gidip istek attığınızda, sistem kimliğinizi otomatik tanır ve ürünü doğrudan sizin hesabınıza bağlayarak kaydeder.


FastAPI - The Complete Course (Beginner + Advanced) eğitim müfredatını, istediğiniz kurallar doğrultusunda (süreleri, ders sayılarını, önizleme yazılarını ve anlamsız satırları temizleyerek) tamamen yapılandırdım. İçeriği özetlemeden, her bir bölümün mantığını, ne işe yaradığını ve nasıl uygulanacağını detaylı konu açıklamaları, kod örnekleri ve pratik uygulamalarla ele aldım.

Eğitim içeriği oldukça kapsamlı ve uzun olduğu için, yönergeniz doğrultusunda ilk büyük parçayı (Giriş, Temel Python, FastAPI Kurulumu ve Proje 1) eksiksiz, derinlemesine ve uygulamalı olarak aşağıda hazırladım.


"Öğrenecekleriniz" Bölümünün Detaylı Açıklaması

Bu kurs, modern backend (arka plan) geliştirme dünyasında en çok tercih edilen Python framework'lerinden biri olan FastAPI'yi sıfırdan uzmanlık seviyesine kadar öğretmeyi amaçlar. "Öğrenecekleriniz" başlığı altındaki maddelerin ifade ettiği yetkinlikler şunlardır:


BÖLÜM 1: Python Kurulumu & Hızlı Tekrar (Python Installation & Refresher)

FastAPI tamamen Python tabanlı ve modern Python özelliklerini (tip belirteçleri, asenkron programlama vb.) yoğun şekilde kullanan bir framework'tür. Bu yüzden güçlü bir API geliştirmeden önce Python'ın temellerine ve Nesne Yönelimli Programlama (OOP) mantığına tam hakim olmak gerekir.

1. Değişkenler ve Kullanıcıdan Girdi Alma (Variables & User Input)

Python'da verileri hafızada tutmak için değişkenleri kullanırız. FastAPI dünyasında, dışarıdan gelen HTTP isteklerindeki verileri işlerken bu değişken tipleri hayati önem taşır.

# Kullanıcıdan string (metin) türünde veri alma
kullanici_adi = input("Lütfen kullanıcı adınızı giriniz: ")

# Kullanıcıdan alınan her girdi varsayılan olarak string'dir. 
# Matematiksel işlem yapabilmek için int() veya float() ile dönüştürmeliyiz.
kullanici_yasi = int(input("Lütfen yaşınızı giriniz: "))

# String Formatting (f-string) kullanımı
gosterim_metni = f"Sisteme Hoş Geldiniz, {kullanici_adi}! 10 yıl sonra {kullanici_yasi + 10} yaşında olacaksınız."
print(gosterim_metni)

2. Veri Yapıları: Listeler, Setler, Demetler ve Sözlükler (Lists, Sets, Tuples, Dictionaries)

API'ler genellikle JSON formatında veri üretir ve tüketir. Python'daki Sözlük (Dictionary) yapısı JSON'ın doğrudan karşılığıdır. Listeler ise veri tabanından çekilen çoklu kayıtları temsil eder.

# Sözlük (Dictionary) yapısı: Key-Value (Anahtar-Değer) ilişkisi
kitap_detay = {
    "id": 1,
    "baslik": "FastAPI Kılavuzu",
    "yazar": "Tarık",
    "sayfa_sayisi": 350,
    "kategoriler": ["Yazılım", "Eğitim"] # Liste (List) yapısı
}

# Demet (Tuple): Değiştirilemez veri listesi (Örn: Sabit durum kodları)
durum_kodlari = ("AKTIF", "PASIF", "ASKIDA")

# Set (Küme): Benzersiz elemanlar tutar
benzersiz_etiketler = {"python", "api", "python"} # Çıktıda sadece bir kez 'python' yer alır.

print(f"Kitap Adı: {kitap_detay['baslik']}")

3. Kontrol Yapıları ve Döngüler (If-Else Statements & Loops)

Gelen isteklerin geçerliliğini kontrol etmek (Örn: Şifre doğru mu? Yaş sınırı geçildi mi?) ve listeler üzerinde dönerek veri ayıklamak için kullanılır.

# Bir todo listesindeki tamamlanmamış görevleri filtreleme uygulaması
görevler = [
    {"baslik": "Kod yaz", "tamamlandi": True},
    {"baslik": "API Test et", "tamamlandi": False},
    {"baslik": "Dokümantasyon oku", "tamamlandi": False}
]

print("Tamamlanması Gereken Görevler:")
for gorev in görevler:
    if not gorev["tamamlandi"]:
        print(f"- {gorev['baslik']}")

4. Fonksiyonlar ve Modüller (Functions & Imports)

Kodun tekrar kullanılabilir olmasını sağlar. FastAPI'de yazdığımız her bir endpoint (uç nokta) aslında özelleştirilmiş birer Python fonksiyonudur.

# İki sayıyı toplayan ve tip belirteci (Type Hinting) kullanan fonksiyon
# Modern Python ve FastAPI bu tip belirteçlerini veri doğrulaması için zorunlu kılar.
def vergi_hesapla(fiyat: float, vergi_orani: float = 0.20) -> float:
    return fiyat + (fiyat * vergi_orani)

toplam_tutar = vergi_hesapla(100.0)
print(f"Toplam Tutar (Vergili): {toplam_tutar}")

5. Nesne Yönelimli Programlama ve Temel Prensipler (OOP, Inheritance, Encapsulation, Polymorphism, Composition)

Büyük projelerde veri tabanı modellerini ve iş mantıklarını sınıflar (Class) halinde tasarlamak gerekir.

class Silah:
    def __init__(self, isim: str, hasar: int):
        self.isim = isim
        self.hasar = hasar

class Kahraman:
    def __init__(self, isim: str, saglik: int, silah: Silah):
        self.isim = isim
        self.saglik = saglik
        self.silah = silah # Composition: Kahraman sınıfı içinde Silah nesnesi barındırılıyor.

    def saldir(self, dusman: 'Kahraman'):
        dusman.saglik -= self.silah.hasar
        print(f"{self.isim}, {self.silah.isim} ile saldırdı! {dusman.isim} {self.silah.hasar} hasar aldı. Kalan Sağlık: {dusman.saglik}")

# Uygulama çalıştırma
kilic = Silah("Efsanevi Kılıç", 25)
buyu_asasi = Silah("Ateş Asası", 35)

savasci = Kahraman("Aragon", 100, kilic)
buyucu = Kahraman("Gandalf", 80, buyu_asasi)

savasci.saldir(buyucu)

BÖLÜM 2: FastAPI Genel Bakış ve Kurulum (FastAPI Overview & Installation)

FastAPI, Python'ın async/await yapısını destekleyen, yüksek performanslı ve otomatik olarak Swagger API dokümantasyonu üreten modern bir web framework'üdür.

1. Sanal Ortam (Virtual Environment) Kurulumu

Projelerimizin bağımlılıklarının bilgisayarımızın işletim sistemindeki küresel Python paketleriyle çakışmaması için her projeye özel izole bir alan (Virtual Environment) oluştururuz.

2. FastAPI ve Uvicorn Kurulumu

FastAPI projesini çalıştırmak için asenkron bir sunucuya (Uvicorn) ihtiyacımız vardır.

pip install fastapi uvicorn

BÖLÜM 3: Proje 1 - FastAPI İstek Metotları Mantığı (Project 1 - FastAPI Request Method Logic)

Bu projede veri tabanı kullanmadan, bellekte (hafızada) tutulan bir kitap listesi üzerinden temel HTTP İstek Metotlarını (GET, POST, PUT, DELETE) ve parametre yönetimini öğreneceğiz.

Tasarım ve Temel Mantık

Eksiksiz Uygulama Kodu (main.py)

Aşağıdaki kodu bilgisayarınızda bir dosyaya kaydedip uvicorn main:app --reload komutuyla çalıştırdığınızda, tarayıcınızdan http://127.0.0 adresine giderek otomatik hazırlanan arayüzden test edebilirsiniz.

from fastapi import FastAPI, Body

app = FastAPI()

# Bellek üzerinde geçici veri tabanı simülasyonu
KITAPLAR = [
    {"id": 1, "baslik": "Python Öğreniyorum", "yazar": "Ahmet", "kategori": "Yazılım"},
    {"id": 2, "baslik": "FastAPI ile Backend", "yazar": "Mehmet", "kategori": "Yazılım"},
    {"id": 3, "baslik": "Suç ve Ceza", "yazar": "Dostoyevski", "kategori": "Edebiyat"},
]

# 1. TÜM KİTAPLARI GETİRME (GET)
@app.get("/kitaplar")
async def tum_kitaplari_getir():
    return KITAPLAR

# 2. YOL PARAMETRESİ İLE TEK BİR KİTAP GETİRME (Path Parameter - GET)
@app.get("/kitaplar/{kitap_id}")
async def kitap_getir(kitap_id: int):
    for kitap in KITAPLAR:
        if kitap["id"] == kitap_id:
            return kitap
    return {"hata": "Aradığınız kitap bulunamadı."}

# 3. SORGU PARAMETRESİ İLE FİLTRELEME (Query Parameter - GET)
# URL Örneği: /kitaplar/filtrele/?kategori=Yazılım
@app.get("/kitaplar/filtrele/")
async def kategoriye_gore_kitaplar(kategori: str):
    eslesen_kitaplar = []
    for kitap in KITAPLAR:
        if kitap["kategori"].lower() == kategori.lower():
            eslesen_kitaplar.append(kitap)
    return eslesen_kitaplar

# 4. YENİ KİTAP EKLEME (POST)
@app.post("/kitaplar/olustur")
async def yeni_kitap_ekle(yeni_kitap=Body()):
    KITAPLAR.append(yeni_kitap)
    return {"mesaj": "Kitap başarıyla eklendi.", "eklenen_kitap": yeni_kitap}

# 5. KİTAP GÜNCELLEME (PUT)
@app.put("/kitaplar/guncelle")
async def kitap_guncelle(guncellenecek_kitap=Body()):
    for i in range(len(KITAPLAR)):
        if KITAPLAR[i]["id"] == guncellenecek_kitap.get("id"):
            KITAPLAR[i] = guncellenecek_kitap
            return {"mesaj": "Kitap başarıyla güncellendi.", "guncel_hali": guncellenecek_kitap}
    return {"hata": "Güncellenmek istenen kitap bulunamadı."}

# 6. KİTAP SİLME (DELETE)
@app.delete("/kitaplar/sil/{kitap_id}")
async def kitap_sil(kitap_id: int):
    for i in range(len(KITAPLAR)):
        if KITAPLAR[i]["id"] == kitap_id:
            silinen = KITAPLAR.pop(i)
            return {"mesaj": "Kitap başarıyla silindi.", "silinen_kitap": silinen}
    return {"hata": "Silinmek istenen kitap bulunamadı."}



BÖLÜM 4: Proje 2 - FastAPI ile Hız Kazanmak (Project 2 - Move Fast with FastAPI)

İlk projemizde verileri doğrulamadan doğrudan Body() sözlüğü olarak kabul etmiştik. Ancak gerçek üretim ortamlarında (production) istemciden gelen verilerin veri tiplerini doğrulamak, eksik verileri yakalamak ve istemciye anlamlı hata mesajları dönmek zorunludur. Bu bölümde Pydantic v2 kütüphanesini kullanarak veri modellemeyi ve HTTP durum kodlarını (Status Codes) yönetmeyi öğreneceğiz.

1. Pydantic ile Veri Doğrulama (Data Validation & Fields)

Pydantic, Python'ın tip belirteçlerini (Type Hinting) kullanarak gelen verileri otomatik olarak doğrular ve dönüştürür. Field yapısı sayesinde alanların alabileceği minimum/maksimum değerleri, karakter sınırlarını ve varsayılan şablonları belirleyebiliriz.

2. HTTP Durum Kodları ve İstisnalar (Status Codes & HTTP Exceptions)

Bir API sadece veri dönmez; işlemin sonucunu standart HTTP durum kodlarıyla bildirir.

Eksiksiz Uygulama Kodu (books2.py)

from typing import Optional
from fastapi import FastAPI, Path, Query, HTTPException, status
from pydantic import BaseModel, Field

app = FastAPI()

# Pydantic Şeması (Veri Doğrulama Modeli)
class KitapIstek(BaseModel):
    id: Optional[int] = Field(description="İsteğe bağlı ID alanı", default=None)
    baslik: str = Field(min_length=3, max_length=100, description="Kitap başlığı en az 3 karakter olmalıdır.")
    yazar: str = Field(min_length=2, description="Yazar adı zorunludur.")
    sayfa_sayisi: int = Field(gt=0, description="Sayfa sayısı 0'dan büyük olmalıdır.")
    puan: int = Field(gt=0, le=5, description="Puan 1 ile 5 arasında olmalıdır.")

    # Swagger arayüzünde varsayılan bir şablon göstermek için yapılandırma
    model_config = {
        "json_schema_extra": {
            "example": {
                "baslik": "Sefiller",
                "yazar": "Victor Hugo",
                "sayfa_sayisi": 1200,
                "puan": 5
            }
        }
    }

# Mock Veri Tabanı
KITAPLAR = [
    {"id": 1, "baslik": "Simyacı", "yazar": "Paulo Coelho", "sayfa_sayisi": 180, "puan": 5},
    {"id": 2, "baslik": "Dune", "yazar": "Frank Herbert", "sayfa_sayisi": 700, "puan": 4}
]

# 1. BELİRLİ BİR KİTABI ID İLE GETİRME VE 404 KONTROLÜ
# Path(gt=0) ile URL'den gelen id parametresinin 0'dan büyük olması zorunlu tutulur.
@app.get("/kitap/{kitap_id}", status_code=status.HTTP_200_OK)
async def kitap_oku(kitap_id: int = Path(gt=0)):
    for kitap in KITAPLAR:
        if kitap["id"] == kitap_id:
            return kitap
    raise HTTPException(status_code=404, detail="Aradığınız kitap bulunamadı.")

# 2. DOĞRULANMIŞ VERİ İLE KİTAP EKLEME (POST - 201 Created)
@app.post("/kitap-ekle", status_code=status.HTTP_201_CREATED)
async def kitap_olustur(yeni_kitap: KitapIstek):
    # Gelen pydantic modelini sözlüğe dönüştürüyoruz
    kitap_sozluk = yeni_kitap.model_dump()
    # ID otomatik atama simülasyonu
    kitap_sozluk["id"] = len(KITAPLAR) + 1 if KITAPLAR else 1
    KITAPLAR.append(kitap_sozluk)
    return {"mesaj": "Kitap başarıyla oluşturuldu.", "veri": kitap_sozluk}

# 3. KİTAP GÜNCELLEME VE HATA YÖNETİMİ
@app.put("/kitap-guncelle/{kitap_id}", status_code=status.HTTP_200_OK)
async def kitap_guncelle(kitap_id: int, guncel_veri: KitapIstek):
    for i, kitap in enumerate(KITAPLAR):
        if kitap["id"] == kitap_id:
            guncel_sozluk = guncel_veri.model_dump()
            guncel_sozluk["id"] = kitap_id  # ID'nin değişmesini engelliyoruz
            KITAPLAR[i] = guncel_sozluk
            return {"mesaj": "Güncelleme başarılı."}
    raise HTTPException(status_code=404, detail="Güncellenmek istenen kitap sistemde yok.")

BÖLÜM 5: Veri Tabanı Kurulumu (SQLAlchemy ORM Setup)

Verileri bellekte tutmak kalıcı bir çözüm değildir. Uygulama her yeniden başladığında veriler silinir. Bu aşamada, Python nesnelerini SQL sorgularına dönüştüren SQLAlchemy ORM (Object-Relational Mapping) yapısını ve hafif bir ilişkisel veri tabanı olan SQLite3 entegrasyonunu tasarlayacağız.

Veri Tabanı Mimarisi Tasarımı

Projemizi ölçeklenebilir kılmak için dosyaları görevlerine göre ayırmalıyız:

  1. database.py: Veri tabanı bağlantısını ve oturum (Session) yönetimini ayarlar.

  2. models.py: Veri tabanındaki tabloların Python sınıfları olarak karşılığını tanımlar.

1. Veri Tabanı Bağlantısı (database.py)

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

# SQLite veri tabanı dosyasının konumu
SQLALCHEMY_DATABASE_URL = "sqlite:///./todosapp.db"

# SQLite özelinde 'check_same_thread' ayarını False yapıyoruz çünkü FastAPI asenkron çalışır
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})

# Veri tabanı işlemlerini yürütecek oturum fabrikası
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Modellerimizin türeyeceği ana sınıf
Base = declarative_base()

# FastAPI bağımlılık enjeksiyonu (Dependency Injection) için oturum fonksiyonu
# İşlem bitince veri tabanı bağlantısını otomatik kapatır.
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

2. Veri Tabanı Tablo Modelleri (models.py)

from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from database import Base

class Kullanicilar(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    kullanici_adi = Column(String, unique=True, index=True)
    hashed_sifre = Column(String)
    is_active = Column(Boolean, default=True)

class Görevler(Base):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    baslik = Column(String)
    aciklama = Column(String)
    tamamlandi = Column(Boolean, default=False)
    # İlişki kurulumu: Her görevin bir sahibi (kullanıcı id'si) olmalıdır.
    sahip_id = Column(Integer, ForeignKey("users.id"))

BÖLÜM 6: Kimlik Doğrulama & Yetkilendirme (Authentication & Authorization)

Uygulamamızdaki görevlerin güvenliğini sağlamak için kullanıcıların kayıt olabilmesi, giriş yapabilmesi ve aldıkları JWT (JSON Web Token) ile güvenli isteklere imza atabilmesi gerekir.

Tasarım Mantığı

  1. Kayıt Olma: Kullanıcı şifresi ham metin (12345) olarak kaydedilemez. Passlib ve BCrypt kullanarak şifreyi geri döndürülemez bir koda (hash) dönüştürürüz.

  2. Giriş Yapma ve Token Üretimi: Kullanıcı adı ve şifre doğruysa, sunucu kullanıcıya kriptografik olarak imzalanmış kısa süreli bir JWT (erişim anahtarı) verir.

  3. Token Doğrulama: Kullanıcı veri tabanına bir görev eklemek istediğinde bu anahtarı isteğin başlığında (Header) sunucuya gönderir. Sunucu anahtarı doğrular ve sahip_id bilgisini token içerisinden çözer.

Eksiksiz Kimlik Doğrulama Uygulaması (auth.py)

Gerekli paketlerin kurulumu:

pip install passlib[bcrypt] pyjwt python-multipart
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from passlib.context import CryptContext
from pydantic import BaseModel
from sqlalchemy.orm import Session
from database import get_db
import models

router = APIRouter(prefix="/auth", tags=["Kimlik Doğrulama"])

# Güvenlik ve Şifreleme Ayarları
SECRET_KEY = "CokGizliKriptografikAnahtarUretimOrtamindaDegistirilmeli"
ALGORITHM = "HS256"
bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# FastAPI'nin token toplama mekanizması (Swagger arayüzünde kilit ikonu açar)
oauth2_bearer = OAuth2PasswordBearer(tokenUrl="auth/token")

class KullaniciOlusturIstek(BaseModel):
    kullanici_adi: str
    email: str
    sifre: str

# Şifre şifreleme ve doğrulama yardımcı fonksiyonları
def sifre_hashle(sifre: str):
    return bcrypt_context.hash(sifre)

def sifre_verifiye_et(duz_sifre, hashlenmis_sifre):
    return bcrypt_context.verify(duz_sifre, hashlenmis_sifre)

# Token üretme fonksiyonu
def token_olustur(kullanici_adi: str, kullanici_id: int, sure: timedelta):
    payload = {
        "sub": kullanici_adi,
        "id": kullanici_id,
        "exp": datetime.now(timezone.utc) + sure
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

# 1. KULLANICI KAYIT ENDPOINT'I
@app.post("/kayit", status_code=status.HTTP_201_CREATED)
async def kullanici_kaydet(db: Annotated[Session, Depends(get_db)], istek: KullaniciOlusturIstek):
    yeni_kullanici = models.Kullanicilar(
        email=istek.email,
        kullanici_adi=istek.kullanici_adi,
        hashed_sifre=sifre_hashle(istek.sifre),
        is_active=True
    )
    db.add(yeni_kullanici)
    db.commit()
    return {"mesaj": "Kullanıcı başarıyla oluşturuldu."}

# 2. GİRİŞ YAPMA VE TOKEN ALMA ENDPOINT'I
@app.post("/token")
async def token_icin_giris_yap(form_verisi: Annotated[OAuth2PasswordRequestForm, Depends()], db: Annotated[Session, Depends(get_db)]):
    kullanici = db.query(models.Kullanicilar).filter(models.Kullanicilar.kullanici_adi == form_verisi.username).first()
    if not kullanici or not sifre_verifiye_et(form_verisi.password, kullanici.hashed_sifre):
        raise HTTPException(status_code=401, detail="Kullanıcı adı veya şifre hatalı.")
    
    token = token_olustur(kullanici.kullanici_adi, kullanici.id, timedelta(minutes=30))
    return {"access_token": token, "token_type": "bearer"}

# 3. KORUMALI ENDPOINT'LER İÇİN MEVCUT KULLANICIYI DOĞRULAYAN BAĞIMLILIK (Dependency)
async def mevcut_kullaniciyi_getir(token: Annotated[str, Depends(oauth2_bearer)]):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        kullanici_adi: str = payload.get("sub")
        kullanici_id: int = payload.get("id")
        if kullanici_adi is None or kullanici_id is None:
            raise HTTPException(status_code=401, detail="Geçersiz token bileşenleri.")
        return {"kullanici_adi": kullanici_adi, "id": kullanici_id}
    except JWTError:
        raise HTTPException(status_code=401, detail="Token doğrulanamadı.")

Korunmuş İstek Örneği (Todos Entegrasyonu)

Artık veri tabanındaki görev ekleme endpoints'lerimizi bu kimlik doğrulamaya bağlayabiliriz. Böylece hiç kimse başkasının Todo görevini göremez veya değiştiremez.

@app.post("/todo/ekle", status_code=status.HTTP_201_CREATED)
async def todo_olustur(
    guncel_kullanici: Annotated[dict, Depends(mevcut_kullaniciyi_getir)], 
    db: Annotated[Session, Depends(get_db)], 
    baslik: str, 
    aciklama: str
):
    # guncel_kullanici dict'i doğrudan JWT'den çözülen güvenli veridir.
    yeni_govev = models.Görevler(
        baslik=baslik,
        aciklama=aciklama,
        tamamlandi=False,
        sahip_id=guncel_kullanici.get("id") # Görev doğrudan giriş yapan kullanıcıya bağlanır.
    )
    db.add(yeni_govev)
    db.commit()
    return {"durum": "Başarılı", "görev_id": yeni_govev.id}



BÖLÜM 7: Proje 3.5 - Alembic Veri Göçleri (Alembic Data Migration)

Canlı ortamdaki (production) bir uygulamada, veri tabanı modellerinde (tablolarda) değişiklik yaptığınızda (örneğin yeni bir sütun ekleme veya veri tipini değiştirme), veri tabanını sıfırlayamazsınız. İçerideki mevcut verileri kaybetmeden veri tabanı şemasını güvenli bir şekilde güncellemek için veri göçü (migration) araçları kullanılır. Python ekosisteminde SQLAlchemy ile en entegre çalışan araç Alembic'tir.

1. Alembic Temel Mantığı ve Komut Şablonları

2. Yapılandırma ve Uygulama Adımları

Öncelikle gerekli kütüphaneyi terminal üzerinden kuruyoruz:

pip install alembic

Ardından projenin ana dizininde Alembic mimarisini başlatıyoruz:

alembic init alembic

Bu komut projenizde alembic.ini dosyası ve alembic/ klasörü oluşturur.

alembic/env.py Dosyasının Yapılandırılması

Alembic'in modellerimizi otomatik algılayabilmesi için env.py dosyası içerisindeki target_metadata alanına kendi modellerimizin metadata bilgisini bağlamamız gerekir.

# alembic/env.py dosyasının içi (İlgili satırlar güncellenir)
from database import Base  # Projemizdeki Base sınıfı içe aktarılır
import models              # Modellerimizin yüklenmesi sağlanır

# Alembic'in tabloları karşılaştırabilmesi için hedef metadata atanır
target_metadata = Base.metadata

Senaryo Uygulaması: Kullanıcılar Tablosuna "telefon_numarasi" Sütunu Ekleme

Modellerimizde (models.py) değişiklik yaptığımızı varsayalım:

# models.py içindeki Kullanicilar sınıfına yeni sütun eklenmesi
class Kullanicilar(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    kullanici_adi = Column(String, unique=True, index=True)
    hashed_sifre = Column(String)
    is_active = Column(Boolean, default=True)
    # Yeni eklenen sütun:
    telefon_numarasi = Column(String, nullable=True)

Terminalden otomatik göç dosyasını oluşturma komutu verilir:

alembic revision --autogenerate -m "kullanicilar tablosuna telefon numarasi eklendi"

Bu komut alembic/versions/ klasörü altında benzersiz bir ID ile yeni bir Python dosyası üretir. İçeriği otomatik olarak şu şekilde tasarlanır:

# alembic/versions/xxxx_kullanicilar_tablosuna_telefon_numarasi_eklendi.py
from alembic import op
import sqlalchemy as sa

def upgrade():
    # Veri tabanına yeni sütunu ekleyen fonksiyon
    op.add_column('users', sa.Column('telefon_numarasi', sa.String(), nullable=True))

def downgrade():
    # Değişikliği geri almak istiyorsak sütunu silen fonksiyon
    op.drop_column('users', 'telefon_numarasi')

Değişikliği veri tabanına yansıtmak (Yükseltmek) için:

alembic upgrade head

Eğer bir sorun çıkarsa ve bu değişikliği geri almak (Düşürmek) istersek:

alembic downgrade -1

BÖLÜM 8: Proje 4 - Pytest ile Birim ve Entegrasyon Testleri (Unit & Integration Testing)

Geliştirilen API endpoints'lerinin canlı ortama çıkmadan önce hatasız çalıştığından emin olmak için test otomasyonu şarttır. Pytest, Python dünyasındaki en popüler test çatısıdır. FastAPI, test işlemleri için Starlette tabanlı bir TestClient sunar.

Test Stratejisi (Mimari Yaklaşım)

Canlı veri tabanını (üretim verilerini) testler sırasında kirletmemek veya bozmamak adına testler için ayrı bir geçici veri tabanı (Test DB) kurgulanmalıdır. Pytest'in fixture mekanizmasını kullanarak her test fonksiyonundan önce temiz bir test veri tabanı oluşturulmasını ve test bitince silinmesini sağlayacağız.

Gerekli paketlerin kurulumu:

pip install pytest httpx

1. Test Yapılandırması (conftest.py)

Pytest, conftest.py isimli dosyayı otomatik olarak tanır. Bu dosyada test süresince kullanılacak ortak bağımlılıklar (fixtures) tanımlanır.

# conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base, get_db
from main import app  # FastAPI uygulama nesneniz

# Testler için izole, geçici bellek içi (in-memory) SQLite veri tabanı
TEST_DATABASE_URL = "sqlite:///./test_todos.db"

engine = 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)
def veri_tabanini_kur hazirla():
    # Test oturumu başladığında tabloları sıfırdan oluşturur
    Base.metadata.create_all(bind=engine)
    yield
    # Bütün testler bittiğinde test veri tabanı tablolarını temizler
    Base.metadata.drop_all(bind=engine)

@pytest.fixture
def db_oturumu():
    # Her test fonksiyonu için izole bir veri tabanı oturumu sağlar
    connection = engine.connect()
    transaction = connection.begin()
    session = TestingSessionLocal(bind=connection)
    
    yield session
    
    session.close()
    transaction.rollback()
    connection.close()

@pytest.fixture
def test_istemcisi(db_oturumu):
    # FastAPI'nin gerçek get_db bağımlılığını, test veri tabanı oturumu ile değiştiriyoruz (Override)
    def override_get_db():
        try:
            yield db_oturumu
        finally:
            pass
            
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)
    # Test bitince bağımlılık enjeksiyonunu eski haline getirir
    app.dependency_overrides.clear()

2. API Uç Noktalarının Test Edilmesi (test_api.py)

Aşağıda hem kimlik doğrulama gerektirmeyen (herkese açık) hem de mock (taklit) verilerle korumalı endpoints'lerin nasıl test edileceğine dair işlevsel senaryolar yer almaktadır.

# test_api.py
import pytest
from fastapi import status

# Senaryo 1: Kitap listeleme endpoint'inin testi (GET)
def test_tum_kitaplari_listele(test_istemcisi):
    # conftest.py dosyasındaki test_istemcisi fixture'ı otomatik enjekte edilir
    yanit = test_istemcisi.get("/kitaplar")
    
    assert yanit.status_code == status.HTTP_200_OK
    assert isinstance(yanit.json(), list)  # Dönen verinin bir liste olduğunu doğrula

# Senaryo 2: Geçersiz ID ile kitap çağrıldığında 404 hatasının doğrulanması
def test_gecersiz_id_ile_kitap_bulunamadi_hatasi(test_istemcisi):
    yanit = test_istemcisi.get("/kitap/9999")
    
    assert yanit.status_code == status.HTTP_404_NOT_FOUND
    assert yanit.json()["detail"] == "Aradığınız kitap bulunamadı."

# Senaryo 3: Pydantic şemasına uymayan eksik veri gönderildiğinde 422 hatasının doğrulanması (POST)
def test_eksik_veri_ile_kitap_olusturma_hatasi(test_istemcisi):
    # 'yazar' ve 'sayfa_sayisi' alanları eksik bırakıldı
    hatali_veri = {
        "baslik": "Eksik Kitap"
    }
    yanit = test_istemcisi.post("/kitap-ekle", json=hatali_veri)
    
    # 422 Unprocessable Entity: FastAPI/Pydantic'in otomatik fırlattığı veri doğrulama hata kodu
    assert yanit.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

# Senaryo 4: Token simülasyonu ile korumalı Todo ekleme rotasının entegrasyon testi
def test_korumali_rotaya_todo_ekleme(test_istemcisi):
    # Korumalı endpoints'leri test ederken 'Authentication & Authorization' katmanını geçmek için
    # test_istemcisi başlığına geçerli şablonda bir Bearer token ekleriz (Mock kullanıcı doğrulaması için)
    headers = {
        "Authorization": "Bearer sahte_veya_test_jwt_token_icerigi"
    }
    
    # Not: Eğer JWT doğrulaması katıysa, conftest içinde JWT kütüphanesiyle 
    # test süresince geçerli gerçek bir test token'ı üretilip buraya basılabilir.
    
    todo_verisi = {
        "baslik": "Test Görevi",
        "aciklama": "Pytest ile sistem entegrasyonu test ediliyor"
    }
    
    # Parametreleri Query veya Body olarak gönderen endpoint tasarımı test edilir
    yanit = test_istemcisi.post(
        "/todo/ekle?baslik=Test%20G%C3%B6revi&aciklama=Pytest%20Entegrasyonu", 
        headers=headers
    )
    
    assert yanit.status_code == status.HTTP_201_CREATED
    assert yanit.json()["durum"] == "Başarılı"

Testleri çalıştırmak için terminale sadece şu komutu yazmanız yeterlidir:

pytest -v

-v (verbose) parametresi, hangi test fonksiyonlarının başarıyla geçtiğini (PASSED) veya kaldığını (FAILED) detaylı bir rapor halinde ekrana basar.




BÖLÜM 9: Proje 5 - Full Stack Uygulama (Full Stack Application)

FastAPI sadece ham veri (JSON) dönen bir API sunucusu olarak değil, aynı zamanda dinamik HTML sayfaları üreten bir Full Stack web sunucusu olarak da çalışabilir. Bu bölümde, Python kodlarımızı HTML sayfalarıyla birleştirmek için Jinja2 şablon motorunu (template engine) kullanacağız. Ayrıca stil işlemleri için CSS ve dinamik etkileşimler için JavaScript entegrasyonu sağlayacağız.

1. Dosya ve Klasör Yapısı (Full Stack Architecture)

Statik dosyaların (CSS/JS) ve HTML şablonlarının düzenli çalışması için proje dizinini şu mimariyle tasarlıyoruz:

├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── main.js
├── templates/
│   ├── layout.html
│   ├── login.html
│   ├── register.html
│   └── todos.html
├── main.py
└── ...

2. Jinja2 ve Statik Dosya Entegrasyonu (main.py)

Gerekli kütüphaneyi kuruyoruz:

pip install jinja2

FastAPI uygulamasında Jinja2 şablon motorunu ve statik dosyaları tanımlıyoruz:

from fastapi import FastAPI, Request, Depends, status
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

app = FastAPI()

# CSS ve JS dosyalarımızın yer aldığı 'static' klasörünü sisteme tanıtıyoruz
app.mount("/static", StaticFiles(directory="static"), name="static")

# HTML şablonlarımızın yer aldığı 'templates' klasörünü bağlıyoruz
templates = Jinja2Templates(directory="templates")

# Örnek Arayüz Rotası (Giriş Sayfası)
@app.get("/login-page", response_class=HTMLResponse)
async def giris_sayfasi(request: Request):
    # Jinja2 şablon motoruna zorunlu olarak 'request' nesnesini geçmeliyiz
    return templates.TemplateResponse("login.html", {"request": request, "sayfa_basligi": "Giriş Yap"})

3. Arayüz Tasarımları (HTML/CSS)

Ana Şablon Katmanı (templates/layout.html)

Tüm sayfalarda ortak olan navigasyon barı, CSS ve JS bağlantılarını tek bir dosyada toplamak için Jinja2'nin kalıtım (inheritance) özelliğini kullanıyoruz.

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ sayfa_basligi }} - TodoApp</title>
    <!-- Ortak CSS Bağlantısı -->
    <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
</head>
<body>
    <nav class="navbar">
        <a href="/todo-page" class="logo">🎯 Görev Yöneticisi</a>
        <div class="nav-links">
            <a href="/login-page">Giriş Yap</a>
            <a href="/register-page">Kayıt Ol</a>
        </div>
    </nav>

    <main class="container">
        <!-- Dinamik olarak değişecek içerik alanı -->
        {% block icerik %}
        {% endblock %}
    </main>

    <!-- Ortak JS Bağlantısı -->
    <script src="{{ url_for('static', path='/js/main.js') }}"></script>
</body>
</html>

Kayıt Olma Sayfası Tasarımı (templates/register.html)

Ana şablondan miras alarak sadece formu yerleştirdiğimiz sayfa tasarımı:

{% extends "layout.html" %}

{% block icerik %}
<div class="form-card">
    <h2>Yeni Hesap Oluştur</h2>
    <form id="registerForm">
        <div class="form-group">
            <label for="username">Kullanıcı Adı</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div class="form-group">
            <label for="email">E-posta</label>
            <input type="email" id="email" name="email" required>
        </div>
        <div class="form-group">
            <label for="password">Şifre</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit" class="btn">Kayıt Ol</button>
    </form>
</div>
{% endblock %}

Stil Dosyası (static/css/style.css)

Uygulamanın profesyonel ve modern görünmesini sağlayacak CSS kodları:

:root {
    --primary-color: #4f46e5;
    --background-color: #f3f4f6;
    --card-color: #ffffff;
    --text-color: #1f2937;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: var(--background-color);
    color: var(--text-color);
    margin: 0;
}

.navbar {
    background-color: var(--primary-color);
    padding: 1rem 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.navbar .logo, .navbar .nav-links a {
    color: white;
    text-decoration: none;
    font-weight: bold;
}

.container {
    max-width: 800px;
    margin: 2rem auto;
    padding: 0 1rem;
}

.form-card {
    background: var(--card-color);
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

.form-group {
    margin-bottom: 1.5rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
}

.form-group input {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
}

.btn {
    background-color: var(--primary-color);
    color: white;
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    width: 100%;
}

JavaScript ile API Etkileşimi (static/js/main.js)

Form verilerini FastAPI backend servislerimize asenkron olarak (fetch ile) göndermek ve kullanıcıyı yönlendirmek için kullanılan JavaScript mantığı:

document.addEventListener("DOMContentLoaded", () => {
    const registerForm = document.getElementById("registerForm");

    if (registerForm) {
        registerForm.addEventListener("submit", async (e) => {
            e.preventDefault(); // Sayfanın yenilenmesini engelle

            const formData = new FormData(registerForm);
            const data = Object.fromEntries(formData.entries());

            // Backend API'mize JSON isteği atıyoruz
            const response = await fetch("/auth/kayit", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    kullanici_adi: data.username,
                    email: data.email,
                    sifre: data.password
                })
            });

            if (response.ok) {
                alert("Kayıt başarılı! Giriş sayfasına yönlendiriliyorsunuz.");
                window.location.href = "/login-page";
            } else {
                const errorData = await response.json();
                alert("Hata: " + (errorData.detail || "Kayıt başarısız."));
            }
        });
    }
});

BÖLÜM 10: Uygulamayı Canlıya Alma (Deploying FastAPI Applications)

Geliştirdiğimiz Full Stack uygulamasını yerel bilgisayarımızdan çıkarıp tüm dünyanın erişebileceği canlı bir bulut sunucusuna (Render) taşıyacağız. Bu süreçte yerel SQLite veri tabanımızı, bulut ortamında çalışan ve üretime hazır (production-ready) bir PostgreSQL veri tabanına dönüştüreceğiz.

1. Canlı Ortam Bağımlılıkları ve Yapılandırma Dosyaları

Canlı sunucunun (Render) projemizi nasıl çalıştıracağını bilmesi ve gerekli Python kütüphanelerini yükleyebilmesi için iki temel dosyaya ihtiyacımız vardır:

requirements.txt Dosyasının Oluşturulması

Projede kullanılan tüm paketleri listeler. PostgreSQL entegrasyonu için sürücü paketi olan psycopg2-binary kütüphanesini de listeye ekliyoruz.

fastapi
uvicorn
sqlalchemy
pydantic
jinja2
passlib[bcrypt]
pyjwt
python-multipart
psycopg2-binary

Çevre Değişkenleri ile Veri Tabanı Seçimi (database.py Güncellemesi)

Yerel geliştirme yaparken SQLite, canlı sunucuda ise Render'ın bize vereceği PostgreSQL bağlantı adresini (DATABASE_URL) dinamik olarak seçmek için Python'ın os modülünü kullanırız.

import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

# Çevre değişkenlerinde DATABASE_URL varsa onu kullan (Render ortamı), yoksa yerel SQLite kullan
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./todosapp.db")

# PostgreSQL URL şeması 'postgres://' ile başlıyorsa, SQLAlchemy v2 uyumluluğu için 'postgresql://' ile değiştirilir
if DATABASE_URL.startswith("postgres://"):
    DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)

# Veri tabanı motoru konfigürasyonu
if "sqlite" in DATABASE_URL:
    engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
    engine = create_engine(DATABASE_URL) # PostgreSQL için ek argümana gerek yoktur

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

2. Git ile Versiyon Kontrolü ve GitHub Adımları

Render, kodları doğrudan bir GitHub deposundan (repository) çekerek canlıya alır. Bu yüzden projemizi Git ile sarmalayıp GitHub'a yüklemeliyiz.

Terminalden uygulanacak adımlar:

# Git deposunu başlat
git init

# Gereksiz dosyaların (venv, test DB, pycache) sunucuya yüklenmesini engellemek için .gitignore oluştur
echo "venv/\n*.db\n__pycache__/" > .gitignore

# Dosyaları sahneye ekle ve ilk kaydı oluştur
git add .
git commit -m "Canlıya alma hazırlığı tamamlandı"

# GitHub üzerinde yeni ve boş bir repository oluşturduktan sonra aşağıdaki komutlarla kodu uzak sunucuya gönderin
git branch -M main
git remote add origin https://github.com
git push -u origin main

3. Render ve Bulut Veri Tabanı (PostgreSQL) Konfigürasyon Adımları

Adım 1: PostgreSQL Veri Tabanı Oluşturma

  1. Render.com adresine giriş yapın ve panelden New > PostgreSQL seçeneğini seçin.

  2. Veri tabanına bir isim verin (todo-prod-db), bölgeyi (Region) seçin ve Create Database butonuna tıklayın.

  3. Veri tabanı oluşturulduğunda panelde yer alan Internal Database URL bilgisini kopyalayın.

Adım 2: Web Service Oluşturma (FastAPI Uygulamasını Yayına Alma)

  1. Render panelinden New > Web Service seçeneğine tıklayın.

  2. GitHub hesabınızı bağlayarak az önce oluşturduğunuz ve kodlarınızı yüklediğiniz depoyu (Repository) seçin.

  3. Açılan konfigürasyon sayfasındaki ayarları şu şekilde doldurun:

  4. Aynı sayfanın altındaki Advanced butonuna tıklayarak Environment Variables (Çevre Değişkenleri) alanına gelin.

  5. En alttaki Create Web Service butonuna tıklayın.

Render önce projeniz için izole bir linux konteyneri kuracak, ardından requirements.txt içindeki paketleri yükleyecek ve uvicorn sunucusunu ayağa kaldıracaktır. Birkaç dakika içinde size https://onrender.com şeklinde dünyanın her yerinden erişilebilir, canlı ve veri tabanı bağlantısı güvenli bir web adresi teslim edecektir.




BÖLÜM 11: İleri Düzey Güvenlik, Performans ve Yönetim Katmanları

Bu bölümde, canlı ortamdaki (production) Full Stack uygulamamızı kurumsal düzeydeki standartlara ulaştırmak için üç kritik mimariyi sisteme entegre edeceğiz:

  1. Güvenlik ve İletişim: E-posta doğrulama ve şifre sıfırlama mekanizmaları.

  2. Yönetim Katmanı: Sadece yönetici (Admin) rolüne sahip kullanıcıların erişebileceği dinamik bir admin paneli arayüzü ve backend mimarisi.

  3. Veri Tabanı Optimizasyonu: Canlıdaki PostgreSQL veri tabanımızın yük altında milisaniyeler içinde yanıt vermesini sağlayacak doğru indeksleme (indexing) stratejileri.


1. E-Posta Doğrulama ve Şifre Sıfırlama Mekanizması

Kullanıcıların sisteme sahte e-postalarla kaydolmasını engellemek ve şifrelerini unuttuklarında güvenli bir şekilde sıfırlayabilmelerini sağlamak için zamana bağlı şifreli token (Timed Token) mimarisi ve arka planda e-posta gönderim işlemlerini üstlenecek FastAPI-Mail entegrasyonu kuracağız.

Gerekli Paketlerin Kurulumu

pip install fastapi-mail

Veri Tabanı Modeline Yeni Alanların Eklenmesi (models.py)

Kullanıcının e-posta doğrulama durumunu takip etmek için is_verified alanını ekliyoruz.

# models.py içindeki Kullanicilar sınıfına ekleme yapın:
class Kullanicilar(Base):
    __tablename__ = "users"
    # ... önceki alanlar ...
    is_verified = Column(Boolean, default=False)  # E-posta doğrulandı mı?

E-Posta Servisi Yapılandırması ve Mantığı (auth_services.py)

FastAPI-Mail yapılandırmasını kuruyoruz ve token üretim mantığını kurguluyoruz. Güvenlik amacıyla, şifre sıfırlama ve e-posta doğrulama token'larının içine kısa bir son kullanma süresi (exp) ekliyoruz.

import os
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from fastapi_mail import ConnectionConfig, FastMail, MessageSchema, MessageType
from fastapi import HTTPException

# E-Posta Sunucu Ayarları (Çevre değişkenlerinden beslenir)
mail_config = ConnectionConfig(
    MAIL_USERNAME=os.getenv("MAIL_USERNAME", "ornek_email@gmail.com"),
    MAIL_PASSWORD=os.getenv("MAIL_PASSWORD", "uygulama_ozel_sifresi_xyz"),
    MAIL_FROM=os.getenv("MAIL_FROM", "ornek_email@gmail.com"),
    MAIL_PORT=587,
    MAIL_SERVER="://gmail.com",
    MAIL_STARTTLS=True,
    MAIL_SSL_TLS=False,
    USE_CREDENTIALS=True,
    VALIDATE_CERTS=True
)

SECRET_KEY = "CokGizliKriptografikAnahtarUretimOrtamindaDegistirilmeli"
ALGORITHM = "HS256"

# E-posta doğrulama veya şifre sıfırlama için 15 dakika geçerli token üretir
def ozel_islem_token_uret(email: str, islem_tipi: str) -> str:
    payload = {
        "sub": email,
        "type": islem_tipi, # "eposta_dogrulama" veya "sifre_sifirlama"
        "exp": datetime.now(timezone.utc) + timedelta(minutes=15)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

# Asenkron olarak kullanıcıya e-posta gönderen fonksiyon
async def asenkron_eposta_gonder(email: str, subject: str, body_html: str):
    message = MessageSchema(
        subject=subject,
        recipients=[email],
        body=body_html,
        subtype=MessageType.html
    )
    fm = FastMail(mail_config)
    await fm.send_message(message)

API Uç Noktaları ve İş Mantığı (auth.py Eki)

Kullanıcı kayıt olduğunda doğrulama e-postası tetiklenir. Şifre sıfırlama istendiğinde ise benzersiz bir link üretilerek gönderilir.

from fastapi import APIRouter, Depends, status, Request
from sqlalchemy.orm import Session
from database import get_db
import models
from auth_services import ozel_islem_token_uret, asenkron_eposta_gonder, SECRET_KEY, ALGORITHM
from jose import jwt, JWTError
from passlib.context import CryptContext

router = APIRouter(prefix="/auth", tags=["Gelişmiş Kimlik Yönetimi"])
bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 1. KAYIT ESNASINDA E-POSTA TETİKLEME (GÜNCELLEME)
# Kullanıcı başarıyla db.add() edildikten hemen sonra çalışır:
async def dogrulama_epostasi_gonder(email: str, request: Request):
    token = ozel_islem_token_uret(email, "eposta_dogrulama")
    # Canlı ortam veya yerel host adresini dinamik yakalar
    ana_url = str(request.base_url) 
    dogrulama_linki = f"{ana_url}auth/dogrula?token={token}"
    
    html_icerik = f"""
    <h3>TodoApp'e Hoş Geldiniz!</h3>
    <p>Hesabınızı aktifleştirmek için lütfen aşağıdaki bağlantıya tıklayın:</p>
    <a href="{dogrulama_linki}" target="_blank">Hesabımı Doğrula</a>
    <p>Bu bağlantı 15 dakika içinde geçerliliğini yitirecektir.</p>
    """
    await asenkron_eposta_gonder(email, "TodoApp Hesap Doğrulama", html_icerik)

# 2. E-POSTA DOĞRULAMA ENDPOINT'I
@router.get("/dogrula")
async def eposta_dogrula(token: str, db: Session = Depends(get_db)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email = payload.get("sub")
        islem_tipi = payload.get("type")
        
        if islem_tipi != "eposta_dogrulama":
            raise HTTPException(status_code=400, detail="Geçersiz işlem tipi.")
            
        kullanici = db.query(models.Kullanicilar).filter(models.Kullanicilar.email == email).first()
        if not kullanici:
            raise HTTPException(status_code=404, detail="Kullanıcı bulunamadı.")
            
        kullanici.is_verified = True
        db.commit()
        return {"mesaj": "E-posta adresiniz başarıyla doğrulandı! Giriş yapabilirsiniz."}
    except JWTError:
        raise HTTPException(status_code=400, detail="Token geçersiz veya süresi dolmuş.")

# 3. ŞİFRE SIFIRLAMA TALEBİ OLUŞTURMA
@router.post("/sifre-sifirlama-talebi")
async def sifre_sifirlama_talebi(email: str, request: Request, db: Session = Depends(get_db)):
    kullanici = db.query(models.Kullanicilar).filter(models.Kullanicilar.email == email).first()
    if not kullanici:
        # Güvenlik nedeniyle "Kullanıcı yok" demek yerine sahte bir başarılı mesajı dönmek siber saldırıları (user enumeration) engeller.
        return {"mesaj": "Eğer e-posta sistemde kayıtlıysa, sıfırlama linki gönderilecektir."}
        
    token = ozel_islem_token_uret(email, "sifre_sifirlama")
    ana_url = str(request.base_url)
    sifrilama_linki = f"{ana_url}auth/sifre-yenile-sayfasi?token={token}"
    
    html_icerik = f"""
    <h3>Şifre Sıfırlama Talebi</h3>
    <p>Yeni şifrenizi belirlemek için lütfen aşağıdaki bağlantıyı kullanın:</p>
    <a href="{sifrilama_linki}">Şifremi Yenile</a>
    """
    await asenkron_eposta_gonder(email, "TodoApp Şifre Sıfırlama", html_icerik)
    return {"mesaj": "Sıfırlama linki e-posta adresinize gönderildi."}

2. Yönetici (Admin) Rolü ve Admin Paneli Arayüz Tasarımı

Sistemde sıradan kullanıcılar ile yöneticileri ayırmak için rol tabanlı yetkilendirme (RBAC) kurgulamamız gerekir. Yönetici, tüm kullanıcıların oluşturduğu görevleri (todos) tek bir ekrandan izleyebilmeli ve gerektiğinde müdahale edebilmelidir.

Veri Tabanı Modeline "Rol" Alanı Eklenmesi (models.py)

# models.py içindeki Kullanicilar sınıfına ekleme:
class Kullanicilar(Base):
    __tablename__ = "users"
    # ...
    rol = Column(String, default="user")  # "user" veya "admin"

Admin Yetki Kontrolü Katmanı (admin.py Backend Mimarisi)

FastAPI bağımlılık enjeksiyonunu (Dependency Injection) kullanarak sadece rolü "admin" olanların geçebileceği bir koruma duvarı oluşturuyoruz.

from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
import models
# 6. Bölümde yazdığımız mevcut kullanıcıyı doğrulamak için import edin
from auth import mevcut_kullaniciyi_getir 

router = APIRouter(prefix="/admin", tags=["Yönetim Paneli"])
templates = Jinja2Templates(directory="templates")

# Admin kontrol fonksiyonu
async def admin_yetki_kontrolu(mevcut_kullanici: dict = Depends(mevcut_kullaniciyi_getir), db: Session = Depends(get_db)):
    kullanici = db.query(models.Kullanicilar).filter(models.Kullanicilar.id == mevcut_kullanici.get("id")).first()
    if not kullanici or kullanici.rol != "admin":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Bu işlem için yetkiniz bulunmamaktadır.")
    return kullanici

# TÜM KULLANICILARIN GÖREVLERİNİ LİSTELEYEN ADMIN PANEL SAYFASI
@router.get("/dashboard", response_class=HTMLResponse)
async def admin_paneli_goster(
    request: Request, 
    db: Session = Depends(get_db), 
    admin_kullanici: models.Kullanicilar = Depends(admin_yetki_kontrolu)
):
    # Veri tabanındaki tüm görevleri sahiplik gözetmeksizin çekiyoruz
    tum_gorevler = db.query(models.Görevler).all()
    # Görevleri ekrana basarken hangi kullanıcının olduğunu göstermek için kullanıcı listesini de çekiyoruz
    tum_kullanicilar = db.query(models.Kullanicilar).all()
    kullanici_haritasi = {k.id: k.kullanici_adi for k in tum_kullanicilar}

    return templates.TemplateResponse("admin_dashboard.html", {
        "request": request, 
        "gorevler": tum_gorevler, 
        "kullanicilar": kullanici_haritasi,
        "admin_adi": admin_kullanici.kullanici_adi,
        "sayfa_basligi": "Sistem Yönetim Paneli"
    })

Admin Paneli Arayüz Tasarımı (templates/admin_dashboard.html)

Tüm sistemdeki görevlerin durumunu, kime ait olduğunu gösteren ve yönetimsel aksiyon butonları barındıran şık, tablo tabanlı bir HTML tasarımı.

{% extends "layout.html" %}

{% block icerik %}
<div class="admin-panel">
    <div class="panel-header">
        <h2>Yönetici Kontrol Paneli</h2>
        <p>Hoş geldiniz, Sayın <strong>{{ admin_adi }}</strong>. Sistemdeki tüm aktiviteleri buradan takip edebilirsiniz.</p>
    </div>

    <table class="admin-table">
        <thead>
            <tr>
                <th>Görev ID</th>
                <th>Sahibi (Kullanıcı)</th>
                <th>Görev Başlığı</th>
                <th>Açıklama</th>
                <th>Durum</th>
                <th>Yönetimsel İşlem</th>
            </tr>
        </thead>
        <tbody>
            {% for gorev in gorevler %}
            <tr>
                <td>#{{ gorev.id }}</td>
                <td><span class="user-badge">{{ kullanicilar.get(gorev.sahip_id, 'Bilinmeyen Kullanıcı') }}</span></td>
                <td><strong>{{ gorev.baslik }}</strong></td>
                <td>{{ gorev.aciklama }}</td>
                <td>
                    {% if gorev.tamamlandi %}
                        <span class="status-badge success">Tamamlandı</span>
                    {% else %}
                        <span class="status-badge warning">Bekliyor</span>
                    {% endif %}
                </td>
                <td>
                    <button class="btn-action delete" onclick="sistemdenGorevSil({{ gorev.id }})">Sistemden Sil</button>
                </td>
            </tr>
            {% else %}
            <tr>
                <td colspan="6" style="text-align: center;">Sistemde henüz kayıtlı görev bulunmamaktadır.</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</div>

<style>
    .panel-header { background: #1e293b; color: white; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; }
    .admin-table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
    .admin-table th, .admin-table td { padding: 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
    .admin-table th { background-color: #4f46e5; color: white; }
    .user-badge { background: #e0e7ff; color: #4338ca; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.85rem; font-weight: bold; }
    .status-badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.85rem; font-weight: bold; }
    .status-badge.success { background: #d1fae5; color: #065f46; }
    .status-badge.warning { background: #fef3c7; color: #92400e; }
    .btn-action.delete { background: #ef4444; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
    .btn-action.delete:hover { background: #dc2626; }
</style>
{% endblock %}

3. Canlıdaki PostgreSQL Üzerinde Veri Tabanı İndeksleme (Indexing) Stratejileri

Uygulamanız büyüdükçe ve kullanıcı sayısı binlere/milyonlara ulaştıkça, veri tabanı tablolarında arama yapmak yavaşlar. PostgreSQL, indeksleme yapılmamış bir tabloda arama yaparken tüm satırları sırayla okur (Sequential Scan). Doğru sütunlara indeks eklemek, sorgu süresini O(N) karmaşıklığından $O(\log N)$ (B-Tree yapısı) seviyesine indirerek performansı yüzlerce kat artırır.

Performans Analizi ve Strateji Belirleme

Hangi sütunların indekslenmesi gerektiğini belirlemek için API sorgularımızın davranışlarını inceleriz:

  1. WHERE kullanıcı_adı = ... veya WHERE email = ... filtreleri kimlik doğrulamada her saniye çalışır. Bunlar Unique Index olmalıdır.

  2. WHERE sahip_id = ... filtresi kullanıcıların sadece kendi Todo listelerini çekmek için sürekli çalışır. Bu yabancı anahtara (Foreign Key) mutlaka indeks atılmalıdır.

  3. Görev aramalarında veya filtrelemelerinde başlık (baslik) üzerinden arama yapılıyorsa, performans için parçasal veya metinsel indeks tasarlanmalıdır.

SQLAlchemy Üzerinde İndekslerin Tanımlanması (models.py Güncellemesi)

SQLAlchemy modellerimizde index=True tanımını eklediğimizde, ORM tablonun oluşturulma aşamasında veya Alembic göçünde PostgreSQL mimarisine uygun optimize CREATE INDEX komutlarını otomatik olarak arka planda çalıştırır.

from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Index
from database import Base

class Kullanicilar(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    # email ve kullanici_adi alanlarında index=True yapıldığında otomatik B-Tree dizini oluşturulur
    email = Column(String, unique=True, index=True)
    kullanici_adi = Column(String, unique=True, index=True)
    hashed_sifre = Column(String)
    is_active = Column(Boolean, default=True)
    is_verified = Column(Boolean, default=False)
    rol = Column(String, default="user")

class Görevler(Base):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    baslik = Column(String, index=True) # Başlığa göre aramalarda (Filter) hızı artırır
    aciklama = Column(String)
    tamamlandi = Column(Boolean, default=False, index=True) # Sadece tamamlananları veya bekleyenleri listelerken performans sağlar
    sahip_id = Column(Integer, ForeignKey("users.id"), index=True) # İlişkili tablolarda JOIN ve WHERE performansını uçurur

    # Çoklu Sütun İndeksi (Composite Index) Tasarımı:
    # Eğer uygulamanızda sıklıkla "belirli bir kullanıcının sadece tamamlanmış görevlerini getir" 
    # şeklinde ikili filtreleme yapılıyorsa, iki sütunu kapsayan tek bir indeks yazmak en performanslı çözümdür.
    __table_args__ = (
        Index('idx_sahip_ve_durum', 'sahip_id', 'tamamlandi'),
    )

Ham SQL ile Canlı PostgreSQL Üzerinde Uygulama ve Doğrulama

Eğer canlı sunucudaki (Render PostgreSQL) mevcut tablolara bu indeksleri kod yazmadan doğrudan ham SQL olarak basmak isterseniz, veri tabanı konsolundan (PgAdmin veya Render CLI ekosisteminden) şu komutları koşturabilirsiniz:

-- 1. Kullanıcı İlişki İndeksi (Foreign Key Optimizasyonu)
CREATE INDEX IF NOT EXISTS idx_todos_sahip_id ON todos(sahip_id);

-- 2. Görev Başlığı İçin Metinsel Arama İndeksi
CREATE INDEX IF NOT EXISTS idx_todos_baslik ON todos(baslik);

-- 3. Performans Doğrulama (Query Plan Analizi)
-- Yazdığınız indeksin PostgreSQL tarafından aktif kullanılıp kullanılmadığını anlamak için sorgunun başına EXPLAIN ANALYZE ekleyin:
EXPLAIN ANALYZE SELECT * FROM todos WHERE sahip_id = 5 AND tamamlandi = false;

EXPLAIN ANALYZE çıktısında "Index Scan using idx_sahip_ve_durum ..." ibaresini görmek, kurguladığınız indeksleme stratejisinin başarıyla çalıştığını ve disk okuma maliyetlerini sıfıra yaklaştırdığını kanıtlar.




BÖLÜM 12: Güvenlik Tahkimatı ve Medya Yönetimi Katmanı

Bu bölümde, web uygulamamızı siber saldırılara karşı korumalı hale getirmek, dosya yönetimini entegre etmek ve kullanıcı hesaplarını en yüksek güvenlik standardına ulaştırmak için üç ileri seviye backend mekanizması tasarlayacağız:

  1. Medya Yönetimi: Görevlere resim veya doküman eklenebilmesi için dosya yükleme (File Upload) mimarisi.

  2. Hız Sınırlandırma (Rate Limiting): Sunucuyu kaba kuvvet (Brute-Force) ve DDoS türevi bot saldırılarından koruyan mekanizma.

  3. İki Faktörlü Kimlik Doğrulama (2FA): Google Authenticator tabanlı, zamana duyarlı tek kullanımlık şifre (TOTP) doğrulama sistemi.


1. Görevlere Dosya Eki (Attachment) Ekleme Özelliği

Kullanıcıların görevlerine belge veya görsel yükleyebilmesi için FastAPI'nin UploadFile yapısını kullanacağız. Güvenlik açığı (Remote Code Execution) oluşmaması adına, gelen dosyaların uzantılarını kontrol eden bir güvenlik filtresi ekleyecek ve dosyaları sunucu üzerinde izole bir klasörde depolayacağız.

Veri Tabanı Modeline Yeni Alanın Eklenmesi (models.py)

# models.py içindeki Görevler sınıfına dosya yolunu tutacak sütunu ekliyoruz:
class Görevler(Base):
    __tablename__ = "todos"
    # ... önceki alanlar ...
    dosya_yolu = Column(String, nullable=True) # Yüklenen dosyanın sunucudaki konumu

Dosya Yükleme ve Güvenlik Filtresi Mantığı (main.py)

import os
import shutil
from uuid import uuid4
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status
from sqlalchemy.orm import Session
from database import get_db
import models
from auth import mevcut_kullaniciyi_getir

router = APIRouter(prefix="/todo", tags=["Görev Medya Yönetimi"])

# İzin verilen güvenli dosya uzantıları
IZIN_VERILEN_UZANTILAR = {".png", ".jpg", ".jpeg", ".pdf", ".docx"}
YUKLEME_KLASORU = "static/uploads"

# Klasör yoksa otomatik oluştur
os.makedirs(YUKLEME_KLASORU, exist_ok=True)

@router.post("/{todo_id}/dosya-yukle")
async def goreve_dosya_ekle(
    todo_id: int,
    file: UploadFile = File(...),
    db: Session = Depends(get_db),
    mevcut_kullanici: dict = Depends(mevcut_kullaniciyi_getir)
):
    # 1. Görevin mevcudiyetini ve sahipliğini kontrol et
    gorev = db.query(models.Görevler).filter(
        models.Görevler.id == todo_id, 
        models.Görevler.sahip_id == mevcut_kullanici.get("id")
    ).first()
    
    if not gorev:
        raise HTTPException(status_code=404, detail="Görev bulunamadı veya yetkiniz yok.")

    # 2. Dosya uzantısı güvenlik kontrolü
    dosya_adi, uzanti = os.path.splitext(file.filename)
    if uzanti.lower() not in IZIN_VERILEN_UZANTILAR:
        raise HTTPException(
            status_code=400, 
            detail=f"Geçersiz dosya türü. İzin verilenler: {', '.join(IZIN_VERILEN_UZANTILAR)}"
        )

    # 3. Dosya adını çakışmaları ve Türkçe karakter sorunlarını önlemek için UUID ile benzersiz yap
    benzersiz_dosya_adi = f"{uuid4()}{uzanti.lower()}"
    hedef_konum = os.path.join(YUKLEME_KLASORU, benzersiz_dosya_adi)

    # 4. Dosyayı sunucu diskine kaydet
    try:
        with open(hedef_konum, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
    finally:
        await file.close()

    # 5. Veri tabanına dosya yolunu kaydet
    gorev.dosya_yolu = f"/static/uploads/{benzersiz_dosya_adi}"
    db.commit()

    return {"mesaj": "Dosya başarıyla yüklendi", "dosya_yolu": gorev.dosya_yolu}

2. API İstek Sınırlandırma (Rate Limiting / DDoS Koruması)

Kötü niyetli botların API uç noktalarımızı saniyede binlerce istekle yormasını (DDoS) veya şifre deneme saldırısı yapmasını (Brute-Force) engellemek için SlowAPI kütüphanesini entegre edeceğiz. SlowAPI, istemcileri IP adreslerine göre takip ederek belirlenen limit aşıldığında istekleri bloke eder.

Gerekli Paketlerin Kurulumu

pip install slowapi

Rate Limiter Entegrasyonu (main.py veya limiter.py)

from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_responder
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

# İstek yapan istemcinin IP adresini baz alan bir sınırlayıcı oluşturuyoruz
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()

# Limit aşımında fırlatılacak istisnayı uygulamaya kaydediyoruz
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_responder)

# ÖRNEK: Dakikada en fazla 5 istek atılabilen korumalı giriş rotası
@app.post("/auth/login")
@limiter.limit("5/minute")
async def giris_yap(request: Request):
    # Bu rota aynı IP adresinden 1 dakika içinde 6. kez çağrılırsa 
    # otomatik olarak HTTP 429 Too Many Requests yanıtı döner.
    return {"mesaj": "Giriş işlemi başarılı."}

# ÖRNEK: Saniyede en fazla 2 istek atılabilen veri çekme rotası
@app.get("/todo/tum-listem")
@limiter.limit("2/second")
async def listele(request: Request):
    return {"mesaj": "Görevler listelendi."}

3. İki Faktörlü Kimlik Doğrulama (2FA / Google Authenticator)

Kullanıcıların şifreleri ele geçirilse bile hesaplarının korunması için Google Authenticator gibi uygulamalarla senkronize çalışan Zamana Duyarlı Tek Kullanımlık Şifre (TOTP) mimarisini kuracağız.

Gerekli Paketlerin Kurulumu

pip install pyotp qrcode

Veri Tabanı Modeline Yeni Alanların Eklenmesi (models.py)

Her kullanıcının kendine özel 2FA anahtarını (Secret Key) ve bu özelliğin aktif olup olmadığını tutacak alanları ekliyoruz.

# models.py içindeki Kullanicilar sınıfına ekleme yapın:
class Kullanicilar(Base):
    __tablename__ = "users"
    # ... önceki alanlar ...
    two_factor_secret = Column(String, nullable=True) # Kriptografik gizli anahtar
    two_factor_enabled = Column(Boolean, default=False) # 2FA aktif mi?

2FA Servis Mantığı (two_factor.py)

Kullanıcıya özel anahtar üretme, Google Authenticator uygulamasına taratabilmesi için QR kod şablonu oluşturma ve uygulamadaki 6 haneli kodu doğrulama işlemlerini yürütecek servis yapısı:

import pyotp

class TwoFactorService:
    @staticmethod
    def yeni_anahtar_uret() -> str:
        # Google Authenticator için rastgele tab32 anahtarı üretir
        return pyotp.random_base32()

    @staticmethod
    def qr_kod_url_uret(kullanici_adi: str, gizli_anahtar: str) -> str:
        # Kullanıcının Authenticator uygulamasında göreceği başlık şablonu
        return pyotp.totp.TOTP(gizli_anahtar).provisioning_uri(
            name=kullanici_adi, 
            issuer_name="TodoApp Şirketi"
        )

    @staticmethod
    def kodu_dogrula(gizli_anahtar: str, girilen_kod: str) -> bool:
        # Kullanıcının telefonundaki 6 haneli kodu doğrular (30 saniyelik zaman toleransı içerir)
        totp = pyotp.totp.TOTP(gizli_anahtar)
        return totp.verify(girilen_kod)

API Katmanına Entegrasyon (auth_2fa.py)

Kullanıcının panelinden 2FA'yı aktifleştirmesi ve giriş yaparken 2FA doğrulamasına tabi tutulması süreçleri:

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from database import get_db
from auth import mevcut_kullaniciyi_getir
from two_factor import TwoFactorService
import models

router = APIRouter(prefix="/2fa", tags=["İki Faktörlü Doğrulama"])

# 1. 2FA ETKİLEŞTİRME TALEBİ (Kullanıcıya QR kod adresi döner)
@router.post("/kurulum-baslat")
async def iki_faktorlu_kurulum(
    mevcut_kullanici: dict = Depends(mevcut_kullaniciyi_getir), 
    db: Session = Depends(get_db)
):
    kullanici = db.query(models.Kullanicilar).filter(models.Kullanicilar.id == mevcut_kullanici.get("id")).first()
    
    # Kullanıcıya özel benzersiz anahtar atanır
    gizli_anahtar = TwoFactorService.yeni_anahtar_uret()
    kullanici.two_factor_secret = gizli_anahtar
    db.commit()

    # Google Authenticator uygulamasının okuyabileceği link üretilir
    qr_url = TwoFactorService.qr_kod_url_uret(kullanici.kullanici_adi, gizli_anahtar)
    
    return {
        "mesaj": "Telefonunuzdaki Google Authenticator uygulamasından QR kodu taratın veya gizli anahtarı girin.",
        "gizli_anahtar": gizli_anahtar,
        "qr_kod_baglantisi": qr_url # Arayüzde bu link qrcode kütüphanesi ile görsele dönüştürülür
    }

# 2. 2FA AKTİFLEŞTİRME ONAYI (Kullanıcı kodu doğru girerse sistemde aktif edilir)
@router.post("/kurulum-onayla")
async def iki_faktorlu_onayla(
    kod: str, 
    mevcut_kullanici: dict = Depends(mevcut_kullaniciyi_getir), 
    db: Session = Depends(get_db)
):
    kullanici = db.query(models.Kullanicilar).filter(models.Kullanicilar.id == mevcut_kullanici.get("id")).first()
    
    if not kullanici.two_factor_secret:
        raise HTTPException(status_code=400, detail="Önce kurulumu başlatmalısınız.")

    # Kod kontrolü
    if TwoFactorService.kodu_dogrula(kullanici.two_factor_secret, kod):
        kullanici.two_factor_enabled = True
        db.commit()
        return {"mesaj": "İki faktörlü kimlik doğrulama hesabınızda başarıyla aktif edildi."}
    
    raise HTTPException(status_code=400, detail="Girdiğiniz 6 haneli kod hatalı veya süresi dolmuş.")

# 3. GİRİŞ SÜRECİNE ENTEGRASYON (GÜNCELLEME MANTIĞI)
# 6. Bölümdeki /token (Giriş) rotasına şu kontrol eklenir:
# if kullanici.two_factor_enabled:
#     return {"status": "2fa_gerekli", "mesaj": "Lütfen 2FA kodunuzu gönderin."}



BÖLÜM 13: Kurumsal Gözlemlenebilirlik (Observability) ve Arka Plan Kuyruk Yönetimi

Bu bölümde, canlı ortamdaki (production) FastAPI uygulamamızı mikrofonksiyonel ve kurumsal düzeyde izlenebilir, hataya dayanıklı (fault-tolerant) ve asenkron işleyişe sahip profesyonel bir ekosisteme dönüştüreceğiz. Üç ana mimariyi sisteme entegre edeceğiz:

  1. Asenkron Kuyruk Yönetimi: Uzun süren işlemleri ana istek-yanıt (request-response) döngüsünden ayıran Celery & Redis mimarisi.

  2. Hata ve Log Yönetimi: Canlı sistemdeki hataları anlık olarak yakalayan Sentry ve yapılandırılmış log üreten Merkezi Günlükleme (Logging) entegrasyonu.

  3. Metrik İzleme (Monitoring): API performansını, istek sayılarını ve yanıt sürelerini ölçüp görselleştiren Prometheus & Grafana altyapısı.


1. Celery & Redis ile Arka Plan Görev Kuyruğu (Asenkron İş Yükleri)

Kullanıcı veri tabanındaki tüm görevlerin özetini içeren bir Excel veya PDF raporu talep ettiğinde, bu işlem binlerce satır veri için birkaç saniye veya dakika sürebilir. İstemciyi HTTP isteğinde bekletmek yerine, işi arka plandaki bir işçi havuzuna (Celery Worker) devrederiz ve mesaj ileticisi (Message Broker) olarak Redis kullanırız.

Gerekli Paketlerin Kurulumu

pip install celery redis pandas openpyxl

Celery Yapılandırması (worker.py)

Ana uygulamadan bağımsız çalışacak Celery istemcisini ve uzun süren örnek bir "Rapor Üretme" görevini tasarlıyoruz.

import os
import time
import pandas as pd
from celery import Celery

# Redis bağlantı adresini çevre değişkenlerinden alıyoruz
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")

# Celery uygulamasını tanımlıyoruz
celery_app = Celery(
    "tasks",
    broker=REDIS_URL,       # Görev kuyruğunun tutulacağı yer
    backend=REDIS_URL       # Görev sonuçlarının saklanacağı yer
)

# Celery konfigürasyon ayarları
celery_app.conf.update(
    task_serializer="json",
    result_serializer="json",
    accept_content=["json"],
    timezone="Europe/Istanbul",
    enable_utc=True
)

@celery_app.task(name="tasks.excel_rapor_uret_task")
def excel_rapor_uret_task(kullanici_id: int, gorevler_listesi: list):
    """
    Bu fonksiyon ana FastAPI thread'ini bloke etmeden 
    arka plandaki Celery Worker üzerinde çalışır.
    """
    print(f"[Worker] Kullanıcı {kullanici_id} için rapor üretimi başladı...")
    
    # Ağır bir verisel işlem simülasyonu (3 saniye sürüyor)
    time.sleep(3)
    
    # Pandas ile veriyi Excel formatına dönüştürme
    df = pd.DataFrame(gorevler_listesi)
    rapor_klasoru = "static/reports"
    os.makedirs(rapor_klasoru, exist_ok=True)
    
    dosya_adi = f"kullanici_{kullanici_id}_rapor_{int(time.time())}.xlsx"
    hedef_yol = os.path.join(rapor_klasoru, dosya_adi)
    
    df.to_excel(hedef_yol, index=False)
    
    print(f"[Worker] Rapor başarıyla üretildi: {hedef_yol}")
    return {"durum": "Tamamlandı", "dosya_yolu": f"/static/reports/{dosya_adi}"}

FastAPI Katmanından Görev Tetikleme (reports_api.py)

Kullanıcı rapor isteği attığında görevi kuyruğa fırlatıp anında bir görev_id teslim eden API uç noktası:

from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from database import get_db
from auth import mevcut_kullaniciyi_getir
import models
from worker import excel_rapor_uret_task

router = APIRouter(prefix="/raporlar", tags=["Asenkron Raporlama"])

@router.post("/excel-tetikle", status_code=status.HTTP_202_ACCEPTED)
async def rapor_uretme_talebi(
    mevcut_kullanici: dict = Depends(mevcut_kullaniciyi_getir),
    db: Session = Depends(get_db)
):
    # 1. Kullanıcının görevlerini veri tabanından çek
    gorevler = db.query(models.Görevler).filter(models.Görevler.sahip_id == mevcut_kullanici.get("id")).all()
    
    # 2. Veriyi Celery'nin serialize edebileceği basit bir liste/sözlük yapısına dönüştür
    seri_gorevler = [
        {"ID": g.id, "Başlık": g.baslik, "Açıklama": g.aciklama, "Durum": g.tamamlandi} 
        for g in gorevler
    ]
    
    # 3. .delay() fonksiyonu ile işi Redis kuyruğuna gönderiyoruz (Anında yanıt döner)
    kuyruk_gorevi = excel_rapor_uret_task.delay(mevcut_kullanici.get("id"), seri_gorevler)
    
    return {
        "mesaj": "Raporlama işlemi arka planda başlatıldı.",
        "görev_id": kuyruk_gorevi.id,
        "kontrol_adresi": f"/raporlar/durum/{kuyruk_gorevi.id}"
    }

@router.get("/durum/{gorev_id}")
async def rapor_durumu_sorgula(gorev_id: str):
    # Kuyruğa gönderilen işin bitip bitmediğini kontrol eden uç nokta
    is_sonucu = excel_rapor_uret_task.AsyncResult(gorev_id)
    if is_sonucu.state == "PENDING":
        return {"durum": "Sırada Bekliyor / İşleniyor"}
    elif is_sonucu.state == "SUCCESS":
        return {"durum": "Başarılı", "sonuc": is_sonucu.result}
    return {"durum": is_sonucu.state}

Celery Worker'ı ayağa kaldırmak için terminalden çalıştırılacak komut:

celery -A worker.celery_app worker --loglevel=info

2. Sentry ve Yapılandırılmış Log Yönetimi (Centralized Logging)

Canlı ortamda (production) kodların hata fırlatması durumunda, sunucu loglarına girip tek tek satır aramak verimsizdir. Sentry entegrasyonu sayesinde oluşan her HTTP 500 veya beklenmedik çökme hatası anında Sentry paneline tüm çağrı yığınıyla (stack trace) birlikte düşer. Aynı zamanda yerel izleme için Python'ın logging modülünü yapılandırılmış (structured) formata getireceğiz.

Gerekli Paketlerin Kurulumu

pip install sentry-sdk[fastapi]

Sentry ve Günlükleme Yapılandırması (logger_setup.py)

import logging
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastAPIIntegration

# Sentry Canlı Sunucu Bağlantısı (DSN adresi Sentry panelinden alınır)
SENTRY_DSN = os.getenv("SENTRY_DSN", "")

if SENTRY_DSN:
    sentry_sdk.init(
        dsn=SENTRY_DSN,
        integrations=[FastAPIIntegration()],
        traces_sample_rate=1.0, # Performans analizi için tüm işlemleri örnekle
        profiles_sample_rate=1.0,
    )

# Standart Python log yapısını özelleştirme
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s",
    handlers=[
        logging.FileHandler("app_production.log", encoding="utf-8"), # Logları dosyaya yazar
        logging.StreamHandler() # Logları terminale yazar
    ]
)

logger = logging.getLogger("TodoAppLogger")

Loglama ve Hata Takip Uygulaması

from fastapi import APIRouter, HTTPException
from logger_setup import logger

router = APIRouter(prefix="/sistem", tags=["Sistem Yönetimi"])

@router.get("/kritik-islem")
async def kritik_islem():
    logger.info("Kritik işlem rotasına bir istek geldi.")
    try:
        # Hata simülasyonu: Sıfıra bölme hatası
        sonuc = 10 / 0
    except ZeroDivisionError as e:
        # Log dosyasına detay yazılır
        logger.error(f"Kritik işlem sırasında beklenmedik hata oluştu: {str(e)}", exc_info=True)
        # Sentry aktiftir, hata otomatik yakalanıp buluta fırlatılır.
        raise HTTPException(status_code=500, detail="İç sunucu hatası oluştu.")

3. Prometheus & Grafana ile Metrik İzleme (Monitoring)

API'mizin sağlığını ölçmek amacıyla; hangi endpoints'e kaç istek geldiğini (Request Count) ve bu isteklerin yanıtlanmasının ne kadar sürdüğünü (Latency) takip etmek için Prometheus biçiminde metrik üreten bir ara katman yazılımı (Middleware) tasarlayacağız.

Gerekli Paketlerin Kurulumu

pip install prometheus-client

Prometheus Middleware Entegrasyonu (metrics.py)

import time
from fastapi import FastAPI, Request
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from fastapi.responses import Response

app = FastAPI()

# Prometheus Metriklerinin Tanımlanması
# 1. İstek Sayacı (Hangi metoda, hangi rotaya, hangi HTTP kodu dönmüş?)
REQUEST_COUNT = Counter(
    "fastapi_requests_total",
    "Toplam API İstek Sayısı",
    labelnames=["method", "endpoint", "http_status"]
)

# 2. İstek Süresi Ölçer (İşlem kaç saniye sürmüş?)
REQUEST_LATENCY = Histogram(
    "fastapi_request_duration_seconds",
    "API İstek Yanıt Süresi Dağılımı",
    labelnames=["method", "endpoint"]
)

# Ara katman yazılımı (Middleware): Her isteğin başında ve sonunda çalışır
@app.middleware("http")
async def prometheus_metrics_middleware(request: Request, call_next):
    baslangic_zamani = time.time()
    
    # İsteğin hangi endpoint'e gittiğini yakala
    endpoint = request.url.path
    method = request.method
    
    # İsteyi bir sonraki katmana/endpoint fonksiyonuna ilet
    yanit = await call_next(request)
    
    # Süreyi hesapla ve kaydet
    gecen_sure = time.time() - baslangic_zamani
    status_code = str(yanit.status_code)
    
    # Prometheus metriklerini güncelle
    REQUEST_COUNT.labels(method=method, endpoint=endpoint, http_status=status_code).inc()
    REQUEST_LATENCY.labels(method=method, endpoint=endpoint).observe(gecen_sure)
    
    return yanit

# Prometheus Sunucusunun Verileri Çekeceği (/metrics) Endpoint'i
@app.get("/metrics")
async def metrics():
    # Prometheus kazıyıcısı (Scraper) bu endpoint'i çağırarak ham metrik verilerini toplar
    return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)

Grafana ve Prometheus Entegrasyon Adımları

  1. Prometheus Kurulumu (prometheus.yml):
    Prometheus sunucunuza uygulamanızın metrik endpoint'ini şu şekilde tanıtırsınız:

    scrape_configs:
      - job_name: 'fastapi-app'
        scrape_interval: 5s
        static_configs:
          - targets: ['localhost:8000'] # FastAPI sunucunuzun adresi
  2. Grafana Görselleştirme:




Bu sistemlerin yerel bilgisayarınızda ya da canlı sunucuda (Render, AWS, DigitalOcean vb.) hiçbir "Bende çalışıyordu, sunucuda çalışmıyor" problemi olmadan ayağa kalkabilmesi için en kararlı yöntem Docker ve Docker Compose mimarisidir.

Aşağıda, geliştirdiğimiz tüm bu katmanları (FastAPI, Celery Worker, Redis ve Prometheus) tek bir hamleyle ayağa kaldıracak profesyonel Docker konfigürasyonlarını, çalıştırma komutlarını ve canlı ortamda en sık karşılaşacağınız konfigürasyon hatalarının çözümlerini hazırladım.


1. Docker Yapılandırma Dosyaları

Projenizin kök (ana) dizininde aşağıdaki dosyaları sırasıyla oluşturun.

Dockerfile (Uygulama İmaj Şablonu)

Hem FastAPI web sunucumuz hem de Celery işçimiz (worker) aynı kod tabanını kullandığı için tek bir Dockerfile ikisine de hizmet edecektir.

# Hafif ve kararlı Python imajı seçiyoruz
FROM python:3.11-slim

# Linux bağımlılıklarını ve PostgreSQL sürücüsü için gerekli paketleri kuruyoruz
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Çalışma dizini oluşturuyoruz
WORKDIR /app

# Bağımlılıkları kopyalayıp yüklüyoruz
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Projenin tüm kodlarını ve klasörlerini (static, templates vb.) konteynere kopyalıyoruz
COPY . .

# Logların terminale anlık düşmesi için Python çıktı tamponlamasını kapatıyoruz
ENV PYTHONUNBUFFERED=1

docker-compose.yml (Çoklu Konteyner Orkestrasyonu)

Tüm servislerin birbiriyle izole bir sanal ağ üzerinden konuşmasını ve doğru sıra ile açılmasını sağlayan orkestrasyon dosyası:

version: '3.8'

services:
  # 1. MESAJ İLETİCİSİ (REDIS)
  redis_broker:
    image: redis:7-alpine
    container_name: todo_redis
    ports:
      - "6379:6379"
    restart: always

  # 2. BACKEND API SUNUCUSU (FASTAPI)
  web_api:
    build: .
    container_name: todo_fastapi
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    ports:
      - "8000:8000"
    environment:
      - REDIS_URL=redis://redis_broker:6379/0
      - DATABASE_URL=sqlite:///./todosapp.db  # Canlıda PostgreSQL adresiyle ezilir
    volumes:
      - .:/app  # Kodda değişiklik yapıldığında konteynerin anlık güncellenmesi için (Hot-Reload)
    depends_on:
      - redis_broker

  # 3. ARKA PLAN İŞÇİSİ (CELERY WORKER)
  celery_worker:
    build: .
    container_name: todo_celery
    command: celery -A worker.celery_app worker --loglevel=info
    environment:
      - REDIS_URL=redis://redis_broker:6379/0
    volumes:
      - .:/app
    depends_on:
      - redis_broker
      - web_api

  # 4. METRİK TOPLAYICI (PROMETHEUS)
  prometheus:
    image: prom/prometheus:latest
    container_name: todo_prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    depends_on:
      - web_api

prometheus.yml (Prometheus Konfigürasyonu)

Docker ağındaki web_api servisinin /metrics uç noktasını kazımak (scrape) için Docker Compose dizinine bu dosyayı ekleyin:

global:
  scrape_interval: 5s

scrape_configs:
  - job_name: 'fastapi-service'
    metrics_path: '/metrics'
    static_configs:
      # Docker Compose içindeki servis ismini direkt domain gibi kullanabiliyoruz
      - targets: ['web_api:8000'] 

2. Sistemi Ayağa Kaldırma Komutları

Gerekli tüm dosyaları hazırladıktan sonra projenizi yerelde ya da sunucuda çalıştırmak için terminalden şu komut zincirini uygulayabilirsiniz:


3. Kritik Konfigürasyon Hataları ve Kesin Çözümleri

Canlı ortama (Render/Docker) geçerken en sık karşılaşılan, saatlerce vakit kaybettirebilecek mimari hatalar ve bunların nokta atışı çözümleri şunlardır:

Hata 1: ConnectionError: Error 111 connecting to localhost:6379. Connection refused. (Celery/Redis Hatası)

Hata 2: Celery Görevleri Tetikliyor Ama Sürekli PENDING Durumunda Kalıyor

Hata 3: OperationalError: (psycopg2.OperationalError) SSL error: certificate verify failed (PostgreSQL Canlı Bağlantı Hatası)

Hata 4: SlowAPI (Rate Limiter) Konteyner İçinde Tüm Kullanıcıları Aynı IP (127.0.0.1) Görüp Herkesi Bloke Ediyor


**********************************************************************************