REST API Development Properly
Bu modülde sadece çalışan endpoint yazmayı değil, sürdürülebilir bir API contract tasarlamayı öğreneceğiz. Controller, DTO, validation, mapping ve HTTP status code kararlarını MiniBank üzerinde bir araya getirerek gerçek bir API katmanı oluşturacağız.
Bir endpoint’in sadece “çalışmasını” değil, doğru contract ile dış dünyaya açılmasını sağlayacaksın.
Controller sorumluluğunu netleştireceksin
Controller’ın business logic yazılan yer değil, HTTP ile application layer arasındaki ince giriş kapısı olduğunu göreceksin.
DTO ve validation kullanacaksın
Entity’yi dışarı açmadan request/response contract’ı tasarlayacak, hatalı input’u kontrollü şekilde yakalayacaksın.
MiniBank API katmanını büyüteceksin
Account endpoint’lerini birlikte yazacak, Customer endpoint’lerini aynı pattern ile katılımcı görevi olarak tamamlayacaksın.
REST API konuşurken aynı kelimeleri aynı anlamda kullanalım.
REST
Basitçe: Kaynakları HTTP üzerinden temsil eden API yaklaşımıdır.
Neden önemli? URL, HTTP method ve status code kararlarını daha tutarlı vermemizi sağlar.
Resource
Basitçe: API’de dışarı açtığımız kavramsal varlıktır; örneğin account, customer, transfer.
Neden önemli? Endpoint tasarımında fiil değil kaynak odaklı düşünmemizi sağlar.
Endpoint
Basitçe: Client’ın çağırdığı URL + HTTP method birleşimidir.
Neden önemli? API contract’ın en görünür parçasıdır; değişirse client’ları etkiler.
DTO
Basitçe: Dış dünyaya veri taşımak için kullanılan sade objedir.
Neden önemli? Entity modelini API contract’tan ayırır.
Request DTO
Basitçe: Client’tan gelen veriyi temsil eder.
Neden önemli? Validation kuralları çoğunlukla burada görünür hale gelir.
Response DTO
Basitçe: Client’a dönen veriyi temsil eder.
Neden önemli? Entity içindeki hassas veya gereksiz alanları dışarı sızdırmadan cevap dönmemizi sağlar.
Validation
Basitçe: Gelen verinin kabul edilebilir olup olmadığını kontrol etmektir.
Neden önemli? Hatalı request’i business logic’e ulaşmadan durdurur.
HTTP Status Code
Basitçe: İsteğin sonucunu makine tarafından okunabilir şekilde anlatan koddur.
Neden önemli? Client’ın başarılı, hatalı, yetkisiz veya bulunamadı gibi durumları doğru yorumlamasını sağlar.
Mapping
Basitçe: Entity, domain object ve DTO arasında veri dönüştürme işlemidir.
Neden önemli? Katmanları birbirine yapıştırmadan API modeli üretmemizi sağlar.
REST API yazmak, controller’a birkaç annotation koymaktan daha fazlasıdır.
Spring Boot ile endpoint açmak kolaydır. Bir class’a @RestController, bir metoda @GetMapping koyduğunda ilk API’n çalışır. Ama senior seviyede asıl soru şudur: Bu API uzun vadede anlaşılır, güvenli, değiştirilebilir ve client dostu kalacak mı?
Controller HTTP dünyasını karşılar, DTO dış contract’ı temsil eder, validation yanlış input’u durdurur, service business işi yapar, response doğru status code ile döner.
5.1 REST API mental model
REST’i ilk aşamada karmaşık bir akademik tanım gibi düşünmeyelim. Bizim için REST, dış dünyanın MiniBank kaynaklarıyla HTTP üzerinden konuşmasıdır. Bir müşteri oluşturmak, bir hesabı görüntülemek, hesap listesini almak veya transfer başlatmak gibi işlemler birer API davranışıdır.
Yeni başlayan ekiplerde sık görülen hata, endpoint’leri method adı gibi tasarlamaktır: /createAccount, /getAccount, /deleteAccount. REST tarafında daha temiz yaklaşım ise kaynağı URL’de, yapılacak işi HTTP method’da anlatmaktır.
Daha zayıf yaklaşım
/createAccount /getAccountById /deleteAccount /updateCustomer
URL fiil gibi davranır. API büyüdükçe isimlendirme dağılır.
Daha temiz yaklaşım
POST /api/accounts
GET /api/accounts/{id}
DELETE /api/accounts/{id}
PUT /api/customers/{id}URL kaynak, HTTP method aksiyon olur. Client için daha öngörülebilirdir.
Bu yaklaşım sadece estetik değildir. API contract net olduğunda frontend, mobil uygulama, başka microservice veya test otomasyonu bu API’yi daha kolay tüketir. Hatalı status code veya dağınık endpoint isimleri küçük projede sorun yaratmayabilir; ama servis sayısı arttıkça operasyonel maliyete dönüşür.
5.2 @RestController ne yapmalı, ne yapmamalı?
@RestController, gelen HTTP request’i Java metoduna bağlayan sınıftır. Ama controller business logic’in merkezi değildir. Controller’ın işi request’i almak, temel request mapping’i sağlamak, validation’ı tetiklemek, service’i çağırmak ve uygun response dönmektir.
Controller içinde hesaplama, transaction kararı, repository çağrısı, external API çağrısı ve karmaşık if blokları birikiyorsa, controller yavaş yavaş application service gibi davranmaya başlamıştır. Bu ileride test ve bakım maliyetini artırır.
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountApplicationService accountApplicationService;
public AccountController(AccountApplicationService accountApplicationService) {
this.accountApplicationService = accountApplicationService;
}
@PostMapping
public ResponseEntity<AccountResponse> create(@Valid @RequestBody AccountCreateRequest request) {
AccountResponse response = accountApplicationService.createAccount(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/{accountNo}")
public ResponseEntity<AccountResponse> getByAccountNo(@PathVariable String accountNo) {
return ResponseEntity.ok(accountApplicationService.getAccount(accountNo));
}
}
5.3 DTO Pattern: Entity dış contract değildir
DTO, API’nin dış dünyaya verdiği sözdür. Entity ise çoğunlukla veritabanı ve persistence modeliyle ilgilidir. Bu ikisini aynı obje üzerinden yönetmek başlangıçta hızlı görünebilir ama uzun vadede ciddi sorun üretir.
Örneğin AccountEntity içinde teknik alanlar, internal status değerleri, audit kolonları veya lazy relation’lar olabilir. Entity’yi doğrudan response olarak dönersen bu alanları istemeden dışarı açabilirsin. Ayrıca entity değiştiğinde API contract’ın da değişmiş olur. Bu, client tarafında beklenmeyen kırılmalara yol açar.
Entity’yi direkt dönmenin riskleri
- Hassas alanlar dışarı sızabilir
- Lazy relation serialization problemi çıkabilir
- API contract persistence modeline bağımlı hale gelir
- Entity değişikliği client kırabilir
DTO kullanmanın kazançları
- API contract kontrollü olur
- Request ve response modeli ayrılır
- Validation daha okunur hale gelir
- Client’a sadece gerekli veri döner
public record AccountCreateRequest(
@NotBlank(message = "Müşteri numarası boş olamaz")
String customerNo,
@NotBlank(message = "Para birimi boş olamaz")
@Pattern(regexp = "TRY|USD|EUR", message = "Para birimi TRY, USD veya EUR olmalıdır")
String currency
) {}
public record AccountResponse(
String accountNo,
String customerNo,
String currency,
BigDecimal balance,
String status
) {}
5.4 Validation: Hatalı request business logic’e ulaşmadan durmalı
Validation iki seviyede düşünülmeli. Birinci seviye request’in şekli ve temel kurallarıdır: alan boş mu, format doğru mu, sayı pozitif mi, uzunluk uygun mu? Bunları request DTO üzerinde annotation ile ifade ederiz. İkinci seviye ise business validation’dır: müşteri gerçekten var mı, hesap aktif mi, transfer limiti yeterli mi? Bunlar service katmanında kalmalıdır.
@Valid çalışırDTO validation
Teknik input kalitesini kontrol eder.
@NotBlank @Size @Pattern @Positive @Email
Business validation
İş kuralını kontrol eder.
Müşteri var mı? Hesap aktif mi? Bakiye yeterli mi? Transfer limiti uygun mu?
@Validated ise özellikle service metotlarında method-level validation veya validation group kullanmak istediğimizde gündeme gelir. Bu modülde controller request validation’ı ana akışımız olacak; sonraki modüllerde business exception handling ile bunu tamamlayacağız.
Account oluştururken müşteri numarası ve para birimi request DTO üzerinde validate edilecek. Müşterinin gerçekten var olup olmadığı ise service katmanının iş kuralı olacak.
5.5 Proper HTTP Status Codes
Status code, API’nin client ile konuştuğu ortak dildir. Body içinde “success: false” yazmak tek başına yeterli değildir. Client önce HTTP seviyesindeki sonucu okur, sonra gerekirse body içindeki detaylara bakar.
200 OK
Başarılı okuma veya başarılı genel işlem.
201 Created
Yeni bir kaynak oluşturulduğunda tercih edilir.
204 No Content
Başarılı işlem var ama response body yok.
400 Bad Request
Request formatı veya validation hatalı.
404 Not Found
İstenen kaynak bulunamadı.
409 Conflict
İstek mevcut durumla çakışıyor; örneğin zaten kayıtlı kaynak.
Her hataya 500 dönmek client açısından “sunucu bozuldu” demektir. Oysa validation hatası 400, bulunamayan kaynak 404, iş kuralı çakışması 409 olabilir. Modül 06’da bu hata cevaplarını global exception handling ile standartlaştıracağız.
5.6 DTO Mapping: Manual mı MapStruct mı?
DTO kullandığımızda kaçınılmaz olarak mapping ihtiyacı doğar. Küçük projelerde manual mapping yeterince açık ve kontrol edilebilirdir. Daha büyük projelerde çok sayıda benzer mapping tekrarına düşüyorsak MapStruct gibi compile-time mapper araçları değerlendirilebilir.
Manual mapping
Küçük ve eğitim amaçlı senaryolarda okunur, bağımlılık gerektirmez, neyin nereye gittiği açıktır.
MapStruct
Çok sayıda DTO dönüşümü olan kurumsal projelerde boilerplate azaltır, compile-time mapping üretir.
@Component
public class AccountDtoMapper {
public AccountResponse toResponse(Account account) {
return new AccountResponse(
account.accountNo(),
account.customerNo(),
account.currency(),
account.balance(),
account.status()
);
}
}
MiniBank eğitiminde ilk aşamada manual mapper kullanacağız. Çünkü amacımız mapping tool öğretmek değil, API contract ve katman ayrımını netleştirmek. İlerleyen aşamada istenirse MapStruct ayrı bir iyileştirme olarak eklenebilir.
MiniBank Case 05 — Account API, DTO & Validation
Bu case’de Account API katmanını birlikte kuracağız. Sonra katılımcılar aynı pattern’i Customer API için tamamlayacak. Görev listesi, doğrulama komutları, kod adımları ve kabul kriterleri Lab Dashboard'da.
Lab sırasında geride kalırsan lab-05-complete branch'ine geçip herkesle aynı checkpoint'ten devam edebilirsin.
Bu modülden akılda kalması gerekenler
REST API, endpoint açmaktan ibaret değildir.
URL, HTTP method, status code, validation ve response modeli birlikte API contract’ı oluşturur.
Controller ince kalmalıdır.
Controller HTTP akışını yönetir; business logic service/application layer’da kalmalıdır.
DTO, entity modelini dış dünyadan ayırır.
Request ve response DTO kullanmak API’yi daha güvenli ve daha stabil hale getirir.
Validation business logic’e ulaşmadan çalışmalıdır.
Temel input hataları DTO seviyesinde durdurulur, iş kuralları service katmanında ele alınır.
Status code client ile ortak dildir.
Doğru status code, API tüketicisinin sonucu doğru yorumlamasını sağlar.
5 kısa kontrol sorusu
1. Entity’yi neden doğrudan response olarak dönmek istemeyiz?
Çünkü entity persistence modelidir; hassas alanları, lazy relation’ları veya internal detayları dışarı sızdırabilir. DTO kullanmak API contract’ı kontrollü hale getirir.
2. Controller’ın ana sorumluluğu nedir?
HTTP request’i almak, DTO/validation akışını başlatmak, service’i çağırmak ve uygun response/status code dönmektir. Business logic controller’da birikmemelidir.
3. @Valid ne zaman çalışır?
Request body DTO’ya dönüştürüldükten sonra, controller metoduna geçmeden önce Bean Validation kurallarını tetikler.
4. Yeni kaynak oluşturulduğunda hangi status code tercih edilir?
Genellikle 201 Created tercih edilir. Response body içinde oluşturulan kaynağın bilgisi dönebilir.
5. DTO mapping için her zaman MapStruct kullanmak zorunda mıyız?
Hayır. Küçük ve eğitim odaklı senaryolarda manual mapping daha açık olabilir. Mapping tekrarları arttığında MapStruct gibi araçlar değerlendirilebilir.