Bir kaynak için CRUD (Create, Read, Update, Delete: oluştur, oku, güncelle, sil) uç noktaları iki adres üzerine kurulur: koleksiyon adresi /kullanicilar ve tekil kaynak adresi /kullanicilar/{id}. Koleksiyona GET listeler, POST yeni kayıt oluşturur ve 201 Created ile birlikte yeni kaydın adresini Location başlığında döndürür. Tekil adrese GET okur, PUT tamamen değiştirir, PATCH kısmen günceller, DELETE siler ve gövde yoksa 204 No Content döner.

Bu yazıda bu eşlemeyi tablo hâlinde veriyor, her satırın arkasındaki kuralı RFC 9110: HTTP Semantics üzerinden açıklıyor ve hepsini FastAPI ile çalışan küçük bir kullanıcı API'sinde gösteriyoruz. Metotların genel tanımı ve güvenli ya da idempotent olma özellikleri HTTP istek ve yanıt yapısı yazısında, PUT ile PATCH'in ayrıntılı farkı PUT ve PATCH yazısında anlatılıyor.

İki adres, beş işlem

İki adres, beş işlem: Listele, Oluştur, GET /kullanicilar/42, Tamamen değiştir

CRUD tasarımının temeli, her kaynak için iki adres tanımlamaktır. Koleksiyon adresi kaynağın bütün üyelerini temsil eder ve yeni üyelerin eklendiği yerdir. Tekil adres koleksiyonun bir üyesini temsil eder ve o üye üzerindeki işlemlerin hedefidir. Hangi metodun hangi adrese uygulandığı aşağıdaki tabloda özetlenir:

İşlemMetot ve adresBaşarılı yanıtTipik hata
ListeleGET /kullanicilar200 OK ve liste401, 403
OluşturPOST /kullanicilar201 Created ve Location400, 409, 422
OkuGET /kullanicilar/42200 OK ve kayıt404
Tamamen değiştirPUT /kullanicilar/42200 OK ya da 204404, 409, 422
Kısmen güncellePATCH /kullanicilar/42200 OK ya da 204404, 422
SilDELETE /kullanicilar/42204 No Content404

Tablonun son sütunu bir başlangıçtır; her senaryoda hangi kodun seçileceği durum kodu seçimi yazısında ayrıntılı olarak ele alınır. Koleksiyon adresine PUT ya da DELETE desteklemek teknik olarak mümkündür ama bütün koleksiyonu değiştirmek ya da silmek nadiren istenen bir işlemdir. Çoğu API bu bileşimleri desteklemez ve 405 Method Not Allowed döner; bu yanıtın kuralları aşağıda.

Oluşturma: POST, 201 ve Location

RFC 9110, bir POST isteği sonucunda sunucuda bir ya da daha fazla kaynak oluştuğunda sunucunun 201 Created yanıtı göndermesi gerektiğini (SHOULD) söyler. Bu yanıt, oluşturulan birincil kaynağın kimliğini veren bir Location başlığı ve isteğin durumunu anlatıp yeni kaynağa atıf yapan bir temsil içermelidir. Pratikte bu, yeni kaydın kendisini gövdede döndürmek anlamına gelir:

http
POST /kullanicilar HTTP/1.1
Content-Type: application/json

{"ad": "Ayşe", "eposta": "ayse@example.com"}
http
HTTP/1.1 201 Created
Location: /kullanicilar/1
Content-Type: application/json

{"id": 1, "ad": "Ayşe", "eposta": "ayse@example.com"}

Location değeri göreli bir adres olabilir; RFC 9110'a göre göreli başvuru hedef URI'ye göre çözülür. 201 yanıtında Location yoksa, oluşturulan birincil kaynak isteğin hedef URI'sidir. Bu ikinci durum PUT ile oluşturmada görülür: istemci adresi zaten bildiği için Location şart değildir.

Oluşturma isteğinin gövdesi de bir sözleşmedir. İstemci JSON gönderdiğini Content-Type: application/json başlığıyla bildirir. RFC 9110, sunucunun içeriği bu metot ve hedef kaynak için desteklenmeyen bir biçimde olduğu için reddettiği durumlar için 415 Unsupported Media Type kodunu tanımlar. Gövde JSON olarak ayrıştırılabiliyor ama alanlar geçersizse başka bir koddan söz ederiz; bu ayrım 400 ve 422 farkı yazısında ele alınır.

Yeni kaydı gövdede döndürmek istemciye iki şey kazandırır: sunucunun ürettiği alanları (id, created_at, varsayılan değerler) ayrı bir GET isteği yapmadan öğrenir ve gönderdiği verinin nasıl kaydedildiğini görür. Yanıt gövdesinin yapısı JSON yanıt yapısı yazısında ele alınır.

Aynı e-posta adresiyle ikinci bir kullanıcı oluşturulmak istendiğinde ne olmalı? RFC 9110, 409 Conflict kodunu isteğin hedef kaynağın güncel durumuyla bir çatışma nedeniyle tamamlanamadığı ve kullanıcının çatışmayı çözüp tekrar gönderebileceği durumlar için tanımlar; sunucunun çatışmanın kaynağını anlamaya yetecek bilgiyi göndermesini de ister. Benzersizlik ihlali bu tanıma uyar ve 409 yaygın bir seçimdir. Ağ hatası yüzünden aynı POST'un iki kez gitmesi ise başka bir sorundur; çözümü Idempotency-Key yazısında.

Okuma: koleksiyon ve tekil kaynak

GET /kullanicilar/42 başarılı olduğunda 200 OK ile kaydın temsilini döndürür. Kayıt yoksa 404 Not Found döner. RFC 9110, 404'ün durumun geçici mi kalıcı mı olduğunu söylemediğini, sunucu durumun kalıcı olduğunu biliyorsa 410 Gone kodunun tercih edildiğini belirtir. Çoğu API silinen kayıtları ayrıca izlemediği için 404 ile yetinir; silinmiş kaydı bilerek 410 ile bildirmek isterseniz bunu belgeleyin.

GET /kullanicilar koleksiyonu döndürür. Burada iki karar vermeniz gerekir: liste gövdede nasıl sarılacak ve uzun listeler nasıl bölünecek. Sınırsız liste döndüren bir koleksiyon, veri büyüdükçe hem sunucuyu hem istemciyi zorlar. Koleksiyonlara baştan sayfalama eklemek için offset sayfalama ve cursor sayfalama yazılarına bakın. Boş koleksiyon bir hata değildir: kayıt yoksa 200 ile boş bir liste dönmek doğru davranıştır.

Güncelleme: PUT ve PATCH

Güncelleme: PUT ve PATCH

PUT, hedef kaynağın durumunu istekteki temsille oluşturmayı ya da değiştirmeyi ister. RFC 9110 burada iki kesin kural koyar: hedef kaynağın güncel bir temsili yoksa ve PUT bir tane oluşturursa sunucu 201 Created göndermek zorundadır (MUST); var olan temsil başarıyla değiştirilirse 200 OK ya da 204 No Content göndermek zorundadır. Yani PUT ile kayıt oluşturmaya izin veren bir API, oluşturma ile değiştirmeyi farklı kodlarla ayırmalıdır.

PUT ile oluşturmaya izin verip vermemek bir tasarım kararıdır. RFC 9110, PUT'un doğru yorumlanmasının istemcinin hangi hedef kaynağı istediğini bilmesine dayandığını ve istemci adına uygun URI'yi sunucunun seçtiği hizmetlerin POST ile kurulması gerektiğini söyler. Kimlikleri sunucu üretiyorsa (otomatik artan sayılar gibi) oluşturma POST ile yapılır ve var olmayan bir kimliğe PUT 404 alır. İstemci kimliği kendisi belirliyorsa (örneğin bir ayar anahtarı ya da dışarıdan gelen bir kod) PUT ile oluşturma doğal hâle gelir.

PATCH ise kaynağın yalnızca bir kısmını değiştirir. PUT'ta eksik gönderilen alanlar kaynağın yeni durumunda da eksik kabul edilirken PATCH'te gönderilmeyen alanlar olduğu gibi kalır. PATCH gövdesinin biçimleri ve iki metodun idempotentlik farkı PUT ve PATCH yazısının konusudur.

Silme: DELETE ve 204

RFC 9110'a göre DELETE başarıyla uygulandığında sunucu üç yanıttan birini göndermelidir: işlem büyük olasılıkla başarılı olacak ama henüz yapılmadıysa 202 Accepted, işlem yapıldıysa ve gönderilecek ek bilgi yoksa 204 No Content, işlem yapıldıysa ve yanıt durumu anlatan bir temsil içeriyorsa 200 OK. En sık seçilen 204'tür; silinen kaydın son hâlini döndürmek isterseniz 200 kullanırsınız. Silme arka planda bir kuyruğa atılıyorsa 202 doğru seçimdir.

DELETE isteğine gövde koymaktan kaçının. RFC 9110, DELETE isteğindeki içeriğin genel olarak tanımlı bir anlamı olmadığını, isteğin anlamını ya da hedefini değiştiremeyeceğini ve bazı uygulamaların bu isteği reddedebileceğini belirtir; istemci, sunucu bunu desteklediğini önceden bildirmedikçe DELETE'e içerik eklememelidir. Silinecek kaydı adres belirler, gövde değil.

Aynı kaydı iki kez silmek ilginç bir soru doğurur. DELETE idempotent bir metottur: iki özdeş isteğin amaçlanan etkisi tek isteğinkiyle aynıdır, ikisinin sonunda da kayıt yoktur. Ama RFC 9110 idempotent bir isteğin tekrarında yanıtın farklı olabileceğini açıkça kabul eder. İkinci silme isteğine 404 dönmek bu yüzden idempotentliği bozmaz; isterseniz kaydın zaten yok olduğunu 204 ile de bildirebilirsiniz. Önemli olan seçiminizi belgelemek ve istemcinin tekrar denediğinde hatayı nasıl yorumlayacağını bilmesidir.

Desteklenmeyen metot: 405 ve Allow

Bir kaynağın desteklemediği bir metotla istek geldiğinde doğru yanıt 405 Method Not Allowed'dır. RFC 9110 bu yanıtta sunucunun, hedef kaynağın o an desteklediği metotları listeleyen bir Allow başlığı üretmesini zorunlu kılar (MUST):

http
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
Content-Type: application/json

{"detail": "Method Not Allowed"}

Aşağıdaki örnekte bu kuralın uygulamada nasıl göründüğüne de bakacağız, çünkü kullandığınız çatının Allow başlığını nasıl doldurduğunu bilmek önemlidir.

Hepsi bir arada: çalışan bir CRUD örneği

Aşağıdaki FastAPI uygulaması kullanıcılar için oluşturma, okuma, değiştirme ve silme uç noktalarını kurar ve test istemcisiyle her birini dener. Veriler bir sözlükte tutulur; gerçek bir uygulamada bu katman veritabanıdır ve kimliği veritabanı üretir.

python
from fastapi import FastAPI, HTTPException, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel

app = FastAPI()
db = {}          # bellek içi örnek; gerçek API'de veritabanı olur
sayac = {"id": 0}

class KullaniciGirdi(BaseModel):
    ad: str
    eposta: str

@app.get("/kullanicilar")
def listele():
    return {"data": list(db.values())}

@app.post("/kullanicilar", status_code=201)
def olustur(girdi: KullaniciGirdi, response: Response):
    sayac["id"] += 1
    kayit = {"id": sayac["id"], **girdi.model_dump()}
    db[kayit["id"]] = kayit
    response.headers["Location"] = f"/kullanicilar/{kayit['id']}"
    return kayit

@app.get("/kullanicilar/{kullanici_id}")
def getir(kullanici_id: int):
    if kullanici_id not in db:
        raise HTTPException(status_code=404, detail="Kullanıcı bulunamadı")
    return db[kullanici_id]

@app.put("/kullanicilar/{kullanici_id}")
def degistir(kullanici_id: int, girdi: KullaniciGirdi):
    if kullanici_id not in db:
        raise HTTPException(status_code=404, detail="Kullanıcı bulunamadı")
    db[kullanici_id] = {"id": kullanici_id, **girdi.model_dump()}
    return db[kullanici_id]

@app.delete("/kullanicilar/{kullanici_id}", status_code=204)
def sil(kullanici_id: int):
    if kullanici_id not in db:
        raise HTTPException(status_code=404, detail="Kullanıcı bulunamadı")
    del db[kullanici_id]
    return Response(status_code=204)

c = TestClient(app)
r = c.post("/kullanicilar", json={"ad": "Ayşe", "eposta": "ayse@example.com"})
print(r.status_code, r.headers["location"])  # çıktı: 201 /kullanicilar/1
print(r.json())  # çıktı: {'id': 1, 'ad': 'Ayşe', 'eposta': 'ayse@example.com'}

r = c.put("/kullanicilar/1", json={"ad": "Ayşe Yılmaz", "eposta": "ayse@example.com"})
print(r.status_code, r.json()["ad"])  # çıktı: 200 Ayşe Yılmaz

r = c.delete("/kullanicilar/1")
print(r.status_code, repr(r.content))  # çıktı: 204 b''

r = c.delete("/kullanicilar/1")
print(r.status_code, r.json())  # çıktı: 404 {'detail': 'Kullanıcı bulunamadı'}

r = c.patch("/kullanicilar")
print(r.status_code, r.headers["allow"])  # çıktı: 405 GET

Çıktıları sırayla okuyalım. Oluşturma isteği 201 aldı ve Location başlığı yeni kaydın adresini verdi; gövde, sunucunun atadığı id alanıyla birlikte kaydın kendisini içeriyor. FastAPI'de bunun için status_code=201 değerini dekoratöre yazmak ve başlığı Response nesnesine eklemek yeterli. PUT isteği var olan kaydı değiştirdi ve 200 ile yeni hâlini döndürdü. Silme 204 aldı ve gövde gerçekten boş: b''. Aynı kaydı ikinci kez silmeye çalışan istek 404 aldı.

Son satır dikkat ister. /kullanicilar adresi hem GET hem POST destekliyor, ama 405 yanıtındaki Allow başlığında yalnızca GET görünüyor. Bunun nedeni, FastAPI'de her dekoratörün ayrı bir rota tanımlaması ve 405 yanıtını üreten yönlendiricinin yolu eşleşen ilk rotanın metotlarını kullanmasıdır: aynı denemede POST rotasını önce tanımladığımızda başlıkta bu kez yalnızca POST göründü. RFC 9110'un istediği tam liste olduğuna göre, Allow başlığının içeriği sizin için önemliyse çatınızın ürettiği yanıtı test edin ve gerekirse 405 yanıtını kendiniz üretin. Hata gövdesini de API'nizin genel hata biçimine uydurmak isteyebilirsiniz; seçenekler Problem Details ve hata mesajı formatı yazılarında.

Tasarım kararları: bir kontrol listesi

Tasarım kararları: bir kontrol listesi: Her kaynak için bir koleksiyon ve bir tekil adres tanımladınız mı?, POST başarılı olduğunda 201, Location ve yeni kaydın temsilini...
  • Her kaynak için bir koleksiyon ve bir tekil adres tanımladınız mı?
  • POST başarılı olduğunda 201, Location ve yeni kaydın temsilini döndürüyor musunuz?
  • Kimlikleri kim üretiyor? Sunucuysa oluşturma POST ile, istemciyse PUT ile de olabilir.
  • PUT ile oluşturmaya izin veriyorsanız oluşturmada 201, değiştirmede 200 ya da 204 dönüyor musunuz?
  • DELETE için 204 mü 200 mü döneceğinize ve tekrarlanan silmede ne olacağına karar verdiniz mi?
  • Desteklenmeyen metotlara 405 ve doğru Allow başlığıyla yanıt veriyor musunuz?
  • Koleksiyon yanıtında sayfalama var mı?

Sık yapılan hatalar

  • Oluşturmada 200 dönmek ve yeni kaydın adresini vermemek: istemci kaydın nerede olduğunu tahmin etmek zorunda kalır.
  • Silmede 204 dönüp gövdeye bir mesaj koymak: 204 gövdesiz bir yanıttır.
  • DELETE isteğinin gövdesinde silinecek kimlikleri göndermek: hedefi adres belirler; çoklu silme için toplu işlem uç noktası tasarlayın.
  • Kayıt bulunamadığında 200 ile boş bir gövde ya da null dönmek.
  • Bütün güncellemeleri PUT ile yapıp istemciden her seferinde tam kaydı istememek: PUT eksik alanları silinmiş sayar.
  • Bir hata durumunu 200 içinde "basarili": false ile bildirmek.

Sık Sorulan Sorular

POST yanıtında gövde döndürmek zorunlu mu?

RFC 9110, 201 yanıtının isteğin durumunu anlatan ve yeni kaynağa atıf yapan bir temsil içermesi gerektiğini söyler. Yeni kaydın kendisini döndürmek bunu karşılamanın en yaygın yoludur.

PUT yanıtında 200 mü, 204 mü dönmeliyim?

İkisi de RFC 9110'a uygundur. Güncel kaydı gövdede göstermek istiyorsanız 200, istemci gönderdiği temsili zaten biliyorsa ve ek bilgi yoksa 204 seçin.

Var olmayan bir kaydı silmek 404 mü, 204 mü dönmeli?

İkisi de savunulabilir. 404, istemciye kaydın zaten olmadığını söyler; 204 ise "istediğin sonuç gerçekleşti" der. Seçiminizi belgeleyin ve bütün kaynaklarda aynı davranışı uygulayın.

Koleksiyonun tamamını silmek için DELETE /kullanicilar kullanabilir miyim?

Teknik olarak mümkündür ama riskli bir işlemdir ve çoğu API bunu desteklemez. Desteklemiyorsanız 405 ve doğru Allow başlığıyla yanıt verin.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: REST API tasarımı öğrenme rehberi · PUT ve PATCH farkı · Durum kodu seçimi · Offset sayfalama · HTTP istek ve yanıt yapısı