REST API Tasarım İlkeleri: Kapsamlı Best Practices Rehberi
Bir API tasarlıyorsunuz ve her şey güzel gidiyor, deployment yapıyorsunuz, ilk kullanıcılar geliyor. Sonra birisi size şunu soruyor: “Kullanıcı silmek için hangi endpoint’i kullanmalıyım, /deleteUser mı yoksa /user/delete mı?” İşte tam bu noktada anlıyorsunuz ki REST API tasarımı sanıldığı kadar basit değil. Yıllar içinde onlarca API projesi gördüm, iyi tasarlanmış olanlar ekiplerin hayatını kurtarırken, kötü tasarlanmış olanlar production’da kabus haline geliyor. Bu yazıda size gerçek dünya deneyimlerimden süzülmüş, pratik REST API tasarım ilkelerini anlatacağım.
REST API Nedir ve Neden Önemli?
REST (Representational State Transfer), HTTP protokolü üzerine inşa edilmiş bir mimari stildir. Bir web servisi değil, bir yaklaşım tarzıdır. Peki neden bu kadar yaygın? Çünkü doğru uygulandığında:
- Öngörülebilir bir yapı sunar, yeni gelen developer ekibe hızlı adapte olur
- Stateless yapısı sayesinde yatay ölçeklendirme kolaylaşır
- Cache mekanizmaları ile performans artırılabilir
- Standart HTTP metodları ile herhangi bir dil/platform uyumlu çalışır
Yanlış uygulandığında ise “REST benzeri bir karmaşa” ortaya çıkar. Bu farkı yaratan detayları inceleyelim.
Resource Tasarımı: URL Yapısı ve Naming Convention
REST’in temel taşı kaynaklardır (resources). Her şey bir kaynak etrafında döner. URL’leriniz bu kaynakları temsil etmeli, eylemleri değil.
Kötü örnekler:
/getUsers/createNewProduct/deleteUserById/updateUserProfile
İyi örnekler:
/users/products/users/{id}/users/{id}/profile
Kaynaklar her zaman çoğul isim olmalı. Bu basit kural bile büyük fark yaratır. HTTP metodu zaten eylemi belirtir, URL’de tekrar eylem yazmak gereksizdir.
Hiyerarşik ilişkileri URL’de gösterin ama çok derine gitmeyin. Üç seviye genellikle yeterlidir:
# İyi - anlaşılır hiyerarşi
GET /users/{userId}/orders/{orderId}
# Kötü - çok derin, yönetimi zor
GET /companies/{companyId}/departments/{deptId}/teams/{teamId}/members/{memberId}/tasks/{taskId}
Derin hiyerarşiler için flat yapı tercih edin ve query parametresi kullanın:
# Derin hiyerarşi yerine
GET /tasks?memberId=123&teamId=456
HTTP Metodlarının Doğru Kullanımı
Bu konuda en çok hata gördüğüm yer burası. Her şey için GET veya POST kullanmak 2000’li yılların alışkanlığı, modern API’lerde bu kabul edilemez.
GET: Veri okuma, side effect yok, idempotent POST: Yeni kaynak oluşturma, non-idempotent PUT: Kaynağın tamamını güncelleme, idempotent PATCH: Kaynağın bir kısmını güncelleme DELETE: Kaynak silme, idempotent
Gerçek bir e-ticaret senaryosunu düşünelim:
# Tüm ürünleri listele
GET /products
# Belirli bir ürünü getir
GET /products/42
# Yeni ürün oluştur
POST /products
Content-Type: application/json
{"name": "Laptop", "price": 1299.99, "stock": 50}
# Ürünü tamamen güncelle (tüm alanlar gerekli)
PUT /products/42
Content-Type: application/json
{"name": "Gaming Laptop", "price": 1499.99, "stock": 45}
# Sadece stok miktarını güncelle
PATCH /products/42
Content-Type: application/json
{"stock": 30}
# Ürünü sil
DELETE /products/42
İdempotency kavramını özellikle vurgulamak istiyorum. Aynı isteği birden fazla kez yapmak aynı sonucu veriyorsa o metod idempotent’tir. PUT ve DELETE idempotent olmalı, POST değildir. Bu önemlidir çünkü network sorunlarında client isteği tekrarlayabilir.
HTTP Durum Kodları: Doğru Kullanım
“Her zaman 200 döndür, hata detayını body’de gönder” yaklaşımı son derece yanlış. HTTP zaten size zengin bir durum kodu seti sunuyor, kullanın.
# Başarılı yanıtlar
200 OK # Genel başarı
201 Created # Yeni kaynak oluşturuldu
204 No Content # Başarılı ama yanıt body'si yok (DELETE için ideal)
# Client hataları
400 Bad Request # Geçersiz istek formatı
401 Unauthorized # Authentication gerekli
403 Forbidden # Yetki yok
404 Not Found # Kaynak bulunamadı
409 Conflict # Çakışma (aynı email ile kayıt gibi)
422 Unprocessable # Validasyon hatası
429 Too Many Requests # Rate limiting
# Server hataları
500 Internal Server Error # Beklenmedik server hatası
502 Bad Gateway # Upstream servis hatası
503 Service Unavailable # Servis geçici olarak kapalı
Bir kullanıcı kaydı senaryosunda yanıt örnekleri:
# Başarılı kullanıcı oluşturma
HTTP/1.1 201 Created
Location: /users/789
Content-Type: application/json
{
"id": 789,
"username": "ahmet_yilmaz",
"email": "[email protected]",
"createdAt": "2024-01-15T10:30:00Z"
}
# Email zaten kayıtlı
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "DUPLICATE_EMAIL",
"message": "Bu email adresi zaten kullanımda",
"field": "email"
}
Versiyonlama Stratejileri
API versiyonlama tartışmalı bir konu. Üç ana yaklaşım var:
URL versiyonlama (en yaygın ve pratik):
GET /v1/users
GET /v2/users
Header versiyonlama:
GET /users
Accept: application/vnd.myapp.v2+json
Query parametre versiyonlama:
GET /users?version=2
Yıllar içinde edindiğim tecrübe URL versiyonlamanın production’da en az sorun çıkardığı yönünde. Neden? Çünkü:
- Log’larda hangi versiyonu kullandığı açıkça görülür
- Browser’da test edilmesi kolaydır
- Proxy ve cache sistemleri kolayca ayırt eder
- Yeni geliştirici anında hangi versiyonda çalıştığını anlar
Versiyonlama yaparken eski versiyonu anında silmeyin. Deprecation süreci şöyle işlemeli:
# Eski versiyonlarda uyarı header'ı ekleyin
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Filtreleme, Sıralama ve Sayfalama
Büyük veri setleri için bu üçü olmazsa olmaz. Production’da milyonlarca kayıt olan bir API’de pagination olmadan neler yaşandığını gördüm, database timeout’ları ve memory spike’ları cabası.
# Filtreleme
GET /products?category=electronics&minPrice=100&maxPrice=500&inStock=true
# Sıralama - field adı önce, yön sonra
GET /products?sort=price&order=asc
GET /products?sort=-price # Eksi işareti descending anlamına gelebilir
# Cursor tabanlı sayfalama (büyük veri setleri için önerilir)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
# Offset tabanlı sayfalama (basit ama büyük offset'lerde yavaşlar)
GET /products?page=3&limit=20&offset=40
Sayfalama yanıtında meta bilgileri de dönün:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [...],
"pagination": {
"total": 1250,
"page": 3,
"limit": 20,
"totalPages": 63,
"nextCursor": "eyJpZCI6MTIwfQ",
"prevCursor": "eyJpZCI6ODBfQ"
},
"links": {
"first": "/products?page=1&limit=20",
"prev": "/products?page=2&limit=20",
"next": "/products?page=4&limit=20",
"last": "/products?page=63&limit=20"
}
}
Hata Yönetimi ve Tutarlı Yanıt Formatı
Tutarlı hata formatı hem developer experience’ı hem de debugging sürecini dramatik olarak iyileştirir. Ekip içinde bir standart belirleyin ve kesinlikle buna uyun.
Önerdiğim hata yanıt yapısı:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Gönderilen veriler doğrulanamadı",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Geçerli bir email adresi giriniz"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Yaş 18-120 arasında olmalıdır",
"constraints": {"min": 18, "max": 120}
}
],
"requestId": "req_8f3k2m9x",
"timestamp": "2024-01-15T10:30:00Z",
"documentation": "https://docs.example.com/errors/VALIDATION_FAILED"
}
}
requestId alanına özellikle dikkat edin. Kullanıcı destek ekibine geldiğinde “şu request ID’yi loglardan arayın” demek hayat kurtarır.
Başarılı yanıtlar için de tutarlı bir wrapper kullanabilirsiniz:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"id": 42,
"name": "Ahmet Yılmaz",
"email": "[email protected]"
},
"meta": {
"requestId": "req_8f3k2m9x",
"processingTime": "45ms"
}
}
Güvenlik: Authentication ve Authorization
API güvenliği ayrı bir yazı konusu olsa da temel prensipleri burada ele almak gerekiyor.
JWT Token kullanımı:
# Login isteği
POST /auth/login
Content-Type: application/json
{"email": "[email protected]", "password": "güçlü_şifre_123"}
# Yanıt
HTTP/1.1 200 OK
{
"accessToken": "eyJhbGciOiJSUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJSUzI1NiJ9...",
"expiresIn": 3600,
"tokenType": "Bearer"
}
# Korumalı endpoint'e istek
GET /users/profile
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
API Key yönetimi için önerilen header formatı:
# Header bazlı API key (query parametresi yerine her zaman header tercih edilmeli)
GET /data
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0
# API key bilgileri dönerken asla tam key'i gösterme
GET /api-keys/key_123
HTTP/1.1 200 OK
{
"id": "key_123",
"name": "Production Key",
"prefix": "sk_live_a1b2",
"lastFourChars": "j0",
"createdAt": "2024-01-01T00:00:00Z",
"lastUsedAt": "2024-01-15T09:00:00Z",
"scopes": ["read:users", "write:products"]
}
Rate limiting header’ları da ekleyin:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1705316400
X-RateLimit-Window: 3600
# Limit aşıldığında
HTTP/1.1 429 Too Many Requests
Retry-After: 847
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
HATEOAS ve Keşfedilebilirlik
HATEOAS (Hypermedia as the Engine of Application State), REST’in en az uygulanan ama çok güçlü bir prensibidir. API yanıtlarınıza ilgili linkler ekleyin:
HTTP/1.1 200 OK
Content-Type: application/hal+json
{
"id": 42,
"username": "ahmet_yilmaz",
"status": "active",
"_links": {
"self": {"href": "/users/42"},
"orders": {"href": "/users/42/orders"},
"profile": {"href": "/users/42/profile"},
"deactivate": {
"href": "/users/42/status",
"method": "PATCH"
}
},
"_actions": {
"canEdit": true,
"canDelete": false,
"canDeactivate": true
}
}
Bu yaklaşım client’ın hardcode URL yazmak zorunda kalmamasını sağlar, API değişikliklerinde client daha az etkilenir.
Content Negotiation ve Response Formatlama
İyi bir API birden fazla format destekler:
# JSON isteği
GET /reports/monthly
Accept: application/json
# CSV formatında rapor isteği
GET /reports/monthly
Accept: text/csv
# XML isteği
GET /reports/monthly
Accept: application/xml
# Desteklenmeyen format
HTTP/1.1 406 Not Acceptable
{
"error": "UNSUPPORTED_FORMAT",
"supportedFormats": ["application/json", "text/csv"]
}
Field selection ile gereksiz veri transferini önleyin:
# Sadece belirli alanları getir
GET /users/42?fields=id,name,email
# Yanıt sadece istenen alanları içerir
{
"id": 42,
"name": "Ahmet Yılmaz",
"email": "[email protected]"
}
Idempotency Key ile Güvenli İşlemler
Ödeme sistemleri gibi kritik operasyonlarda Idempotency-Key header’ı hayat kurtarır. Network kesintisinde aynı isteği tekrar gönderseniz bile işlem ikinci kez çalışmaz:
# Ödeme isteği - idempotency key ile
POST /payments
Idempotency-Key: pay_7f3k2m9x_1705316400
Content-Type: application/json
{
"amount": 299.99,
"currency": "TRY",
"customerId": 42,
"orderId": 789
}
# Aynı key ile tekrar istek gönderilirse
# Server işlemi tekrar yapmaz, önceki sonucu döner
HTTP/1.1 200 OK
X-Idempotent-Replayed: true
{
"paymentId": "pay_abc123",
"status": "completed",
"amount": 299.99
}
API Dokümantasyonu ve OpenAPI
Kod kadar önemli olan şey dokümantasyon. OpenAPI (Swagger) standardını kullanın. Basit bir endpoint dokümantasyonu:
# OpenAPI spec'in serve edilmesi
GET /api-docs
GET /api-docs/swagger.json
GET /api-docs/openapi.yaml
# Swagger UI
GET /api-docs/ui
# Postman collection export
GET /api-docs/postman
Dokümantasyon için temel prensipler:
- Her endpoint için açıklayıcı summary ve description yazın
- Tüm query parametrelerini, request body ve response schema’larını belgeleyin
- Hata kodlarını ve ne zaman döndüklerini açıklayın
- Gerçek request/response örnekleri ekleyin
- Changelog tutun, hangi versiyonda ne değişti belgelensin
Performans ve Önbellek
HTTP cache mekanizmalarını kullanın:
# Cache kontrolü
GET /products/42
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"
Last-Modified: Mon, 15 Jan 2024 10:00:00 GMT
Cache-Control: public, max-age=3600
# Conditional GET - değişmediyse 304 döner
GET /products/42
If-None-Match: "a1b2c3d4e5f6"
If-Modified-Since: Mon, 15 Jan 2024 10:00:00 GMT
HTTP/1.1 304 Not Modified
N+1 problemi için composite endpoint’ler veya query parametreleri kullanın:
# Kötü - her sipariş için ayrı user isteği
GET /orders # 50 sipariş döner, sonra her biri için:
GET /users/1
GET /users/2
...
# İyi - tek istekte ilişkili veriyi getir
GET /orders?include=user,items,shipping
Gerçek Dünya Checklist’i
API’nizi production’a almadan önce şunları kontrol edin:
- Resource naming: Tüm endpoint’ler çoğul isim mi?
- HTTP metodları: Doğru metodlar kullanılıyor mu?
- Durum kodları: Semantik olarak doğru mu?
- Versiyonlama: Strateji belirlendi ve tutarlı uygulandı mı?
- Pagination: Tüm liste endpoint’lerinde var mı?
- Authentication: JWT veya API key doğru implement edildi mi?
- Rate limiting: Header’lar dahil aktif mi?
- Error format: Tüm hatalarda tutarlı format var mı?
- HTTPS: Her ortamda zorunlu mu?
- Logging: Her request için requestId loglanıyor mu?
- Dokümantasyon: OpenAPI spec güncel mi?
- Input validation: Tüm user input’lar validate ediliyor mu?
Sonuç
İyi bir REST API tasarlamak tek seferlik bir iş değil, sürekli gelişen bir süreçtir. Bu yazıda anlattığım prensiplerin hepsini aynı anda uygulamaya çalışmak yerine önce temel ilkeleri sağlamlaştırın: tutarlı resource isimlendirme, doğru HTTP metodları, anlamlı durum kodları ve versiyonlama stratejisi. Bunlar olmadan diğer iyileştirmeler havada kalır.
Ekibinizle API tasarım kararlarını dokümante edin. “Neden böyle yaptık?” sorusunun cevabını 6 ay sonra bulmak için commit log’larını karıştırmak zorunda kalmamalısınız. Architecture Decision Record (ADR) tutun.
Son olarak, API’nizi en büyük eleştirmenin kendiniz olun. “Bu endpoint mantıklı mı?”, “Bunu ilk kez gören bir developer ne düşünür?” sorularını sürekli sorun. Çünkü iyi bir API, dokümantasyon olmadan da nasıl çalıştığı anlaşılan API’dir.
