ResponseEntity
REST API geliştirirken sadece veri döndürmek yetmez — HTTP durum kodları, response header'ları ve yanıt gövdesinin tamamını kontrol etmeniz gerekir. ResponseEntity<T>, Spring MVC'de HTTP yanıtını (status code + headers + body) tam olarak kontrol etmenizi sağlayan güçlü bir sınıftır.
Bir restoranda yemek sipariş ettiğinizi düşünün. Garson size sadece yemeği getirmez — tabağın yanında servis bilgisi (header'lar), yemeğin durumu (status code: hazır, bekleniyor, tükendi) ve tabağın kendisi (body) vardır. ResponseEntity tam da bu bütünlüğü sağlar — sadece veri değil, HTTP yanıtının tüm bileşenlerini kontrol eder.
Neden ResponseEntity Kullanmalıyız?
Basit bir @RestController metodundan Java nesnesi döndürdüğünüzde, Spring otomatik olarak 200 OK status kodu ile JSON yanıtı gönderir. Ancak gerçek dünyada farklı senaryolara farklı yanıtlar vermeniz gerekir:
// ResponseEntity KULLANMADAN — her zaman 200 OK döner
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
User user = userService.findById(id);
// user null ise ne olacak? 200 OK ile null mu dönecek?
// İstemci "200 OK ama body boş" durumunu nasıl yorumlayacak?
return user;
}
// ResponseEntity KULLANARAK — anlamlı HTTP yanıtı
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok) // 200 OK + user body
.orElse(ResponseEntity.notFound().build()); // 404 Not Found, body yok
}İlk yaklaşımda istemci null body ile 200 alır ve "başarılı ama veri yok mu, yoksa hata mı?" diye düşünür. İkinci yaklaşımda 404 alır ve net olarak "kullanıcı bulunamadı" anlar.
HTTP Durum Kodları — Hızlı Referans
REST API'lerde doğru durum kodlarını kullanmak, API'nizin profesyonellik seviyesini belirler:
| Kod | İsim | Kullanım | Açıklama |
|---|---|---|---|
| 200 | OK | Başarılı GET, PUT, PATCH | Genel başarı yanıtı |
| 201 | Created | Başarılı POST | Yeni kaynak oluşturuldu |
| 204 | No Content | Başarılı DELETE | Yanıt gövdesi yok |
| 400 | Bad Request | Geçersiz istek | Validation hatası, eksik alan |
| 401 | Unauthorized | Kimlik doğrulama gerekli | Token yok veya geçersiz |
| 403 | Forbidden | Yetki yok | Kimlik doğru ama yetki yetersiz |
| 404 | Not Found | Kaynak bulunamadı | ID veya URL geçersiz |
| 409 | Conflict | Çakışma | Duplicate email, eşzamanlı güncelleme |
| 422 | Unprocessable Entity | İş mantığı hatası | Yetersiz bakiye, geçersiz durum geçişi |
| 429 | Too Many Requests | Rate limit aşıldı | Çok fazla istek gönderildi |
| 500 | Internal Server Error | Sunucu hatası | Beklenmeyen hata, bug |
💡 İpucu: 2xx başarı, 4xx istemci hatası, 5xx sunucu hatası anlamına gelir. API'nizde 5xx yanıtları minimum olmalı — istemci hataları (4xx) ile sunucu hatalarını (5xx) karıştırmayın.
ResponseEntity Oluşturma Yöntemleri
1. Static Factory Metodları (Önerilen)
Spring, yaygın durum kodları için hazır factory metodları sunar. Bu yaklaşım en okunabilir ve en temiz olandır:
// 200 OK + body
ResponseEntity.ok(user);
ResponseEntity.ok().body(user);
ResponseEntity.ok()
.header("X-Custom-Header", "value")
.body(user);
// 201 Created + Location header
ResponseEntity.created(URI.create("/api/users/" + user.getId()))
.body(user);
// 204 No Content (DELETE işlemleri için ideal)
ResponseEntity.noContent().build();
// 404 Not Found (body yok)
ResponseEntity.notFound().build();
// 400 Bad Request
ResponseEntity.badRequest().body(errorResponse);
// 202 Accepted (asenkron işlemler için)
ResponseEntity.accepted().body(asyncResult);
// Custom status kodu
ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", "60")
.build();
// Unprocessable Entity (iş mantığı hataları için)
ResponseEntity.unprocessableEntity().body(validationErrors);2. Constructor ile
// new ResponseEntity<>(body, headers, status)
HttpHeaders headers = new HttpHeaders();
headers.add("X-Custom-Header", "my-value");
headers.add("X-Request-Id", UUID.randomUUID().toString());
new ResponseEntity<>(user, headers, HttpStatus.OK);
// Sadece status
new ResponseEntity<>(HttpStatus.NO_CONTENT);
// Body + status
new ResponseEntity<>(user, HttpStatus.CREATED);⚠️ Dikkat: Static factory metodları daha okunabilirdir. Constructor yaklaşımını sadece factory metodlarının kapsamadığı nadir durumlarda kullanın.
CRUD Operasyonlarında ResponseEntity Kullanımı
Tam bir CRUD controller'ında ResponseEntity nasıl kullanılır — her endpoint için doğru status kodu:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// GET /api/users — Tüm kullanıcıları listele
@GetMapping
public ResponseEntity<List<UserDto>> getAllUsers(
@RequestParam(required = false) String role,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
List<UserDto> users = userService.findAll(role, page, size);
long totalCount = userService.count(role);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(totalCount))
.header("X-Page", String.valueOf(page))
.header("X-Page-Size", String.valueOf(size))
.body(users);
}
// GET /api/users/42 — Tek kullanıcı getir
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404
}
// POST /api/users — Yeni kullanıcı oluştur
@PostMapping
public ResponseEntity<UserDto> createUser(
@RequestBody @Valid CreateUserRequest request) {
UserDto created = userService.create(request);
URI location = URI.create("/api/users/" + created.getId());
return ResponseEntity.created(location).body(created); // 201 + Location header
}
// PUT /api/users/42 — Kullanıcı güncelle (tam)
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@RequestBody @Valid UpdateUserRequest request) {
return userService.update(id, request)
.map(ResponseEntity::ok) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404
}
// PATCH /api/users/42 — Kullanıcı kısmi güncelleme
@PatchMapping("/{id}")
public ResponseEntity<UserDto> patchUser(
@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
return userService.partialUpdate(id, updates)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/users/42 — Kullanıcı sil
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.delete(id)) {
return ResponseEntity.noContent().build(); // 204 No Content
}
return ResponseEntity.notFound().build(); // 404 Not Found
}
}Önemli noktalar:
POST→ 201 Created +Locationheader (yeni kaynağın URL'i)GET→ 200 OK (başarılı), 404 Not Found (bulunamadı)PUT/PATCH→ 200 OK (güncellendi), 404 Not Found (bulunamadı)DELETE→ 204 No Content (silindi, body yok), 404 Not Found
Custom Header Ekleme
@GetMapping("/download/{fileId}")
public ResponseEntity<byte[]> downloadFile(@PathVariable Long fileId) {
FileData file = fileService.getFile(fileId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", file.getFilename());
headers.setContentLength(file.getSize());
headers.add("X-File-Hash", file.getHash());
return new ResponseEntity<>(file.getData(), headers, HttpStatus.OK);
}
// Cache-Control header'ı
@GetMapping("/config")
public ResponseEntity<AppConfig> getConfig() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.eTag("v1")
.lastModified(Instant.now())
.body(configService.getConfig());
}
// Pagination header'ları
@GetMapping("/products")
public ResponseEntity<List<ProductDto>> getProducts(Pageable pageable) {
Page<ProductDto> page = productService.findAll(pageable);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(page.getTotalElements()))
.header("X-Total-Pages", String.valueOf(page.getTotalPages()))
.header("X-Current-Page", String.valueOf(page.getNumber()))
.header("X-Page-Size", String.valueOf(page.getSize()))
.body(page.getContent());
}Generic API Response Wrapper
Tutarlı API yanıtları için bir wrapper sınıfı oluşturmak yaygın ve çok güçlü bir best practice'tir. Tüm API endpoint'leri aynı formatta yanıt döner:
// Generic Response Wrapper
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private LocalDateTime timestamp;
private String traceId;
private ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
this.timestamp = LocalDateTime.now();
this.traceId = MDC.get("traceId"); // Distributed tracing
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "İşlem başarılı", data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
// Getter'lar...
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public T getData() { return data; }
public LocalDateTime getTimestamp() { return timestamp; }
public String getTraceId() { return traceId; }
}Kullanımı:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<UserDto>> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(ApiResponse.success(user)))
.orElse(ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("Kullanıcı bulunamadı")));
}
@PostMapping
public ResponseEntity<ApiResponse<UserDto>> createUser(
@RequestBody @Valid CreateUserRequest req) {
UserDto created = userService.create(req);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success("Kullanıcı oluşturuldu", created));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.ok(ApiResponse.success("Kullanıcı silindi", null));
}
}Bu yaklaşımla API yanıtlarınız tutarlı bir formatta olur:
{
"success": true,
"message": "İşlem başarılı",
"data": {
"id": 42,
"name": "Ahmet",
"email": "ahmet@mail.com"
},
"timestamp": "2024-03-15T14:30:00",
"traceId": "abc-123-def"
}
{
"success": false,
"message": "Kullanıcı bulunamadı",
"data": null,
"timestamp": "2024-03-15T14:30:05",
"traceId": "abc-124-ghi"
}Error Response Wrapper
Hata yanıtları için ayrı bir sınıf kullanmak daha temizdir:
public class ErrorResponse {
private int status;
private String error;
private String message;
private String path;
private LocalDateTime timestamp;
private List<FieldError> fieldErrors;
@lombok.Data
@lombok.AllArgsConstructor
public static class FieldError {
private String field;
private String message;
private Object rejectedValue;
}
// Builder veya static factory...
public static ErrorResponse of(HttpStatus status, String message, String path) {
ErrorResponse error = new ErrorResponse();
error.status = status.value();
error.error = status.getReasonPhrase();
error.message = message;
error.path = path;
error.timestamp = LocalDateTime.now();
return error;
}
}
// Global Exception Handler ile birlikte
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
UserNotFoundException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.of(
HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.of(
HttpStatus.BAD_REQUEST, "Validation hatası", request.getRequestURI());
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors().stream()
.map(fe -> new ErrorResponse.FieldError(
fe.getField(), fe.getDefaultMessage(), fe.getRejectedValue()))
.toList();
error.setFieldErrors(fieldErrors);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicate(
DuplicateEmailException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.of(
HttpStatus.CONFLICT, ex.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
}Conditional Response — ETag ve Cache
@GetMapping("/{id}")
public ResponseEntity<ProductDto> getProduct(
@PathVariable Long id,
WebRequest webRequest) {
ProductDto product = productService.findById(id);
String etag = "\"" + product.getVersion() + "\"";
// ETag kontrolü — değişmediyse 304 Not Modified döndür
if (webRequest.checkNotModified(etag)) {
return null; // Spring otomatik 304 döner
}
return ResponseEntity.ok()
.eTag(etag)
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES))
.body(product);
}ResponseEntity vs Direkt Nesne Döndürme
| Özellik | Direkt Nesne | ResponseEntity |
|---|---|---|
| Status kodu | Her zaman 200 | Tam kontrol |
| Header ekleme | ❌ | ✅ |
| Conditional response | Zor | Kolay |
| Null handling | Sorunlu | Temiz 404 |
| File download | ❌ | ✅ |
| Cache control | ❌ | ✅ |
| Kullanım kolaylığı | Çok basit | Biraz daha verbose |
Kural: Basit GET endpoint'lerinde direkt nesne döndürebilirsiniz. Ama POST, PUT, DELETE ve hata senaryolarında ResponseEntity kullanın. Tutarlılık açısından her yerde ResponseEntity kullanmak en güvenli yaklaşımdır.
Streaming Response — Büyük Dosyalar
Büyük dosyaları memory'ye yüklemeden stream olarak döndürmek için StreamingResponseBody kullanabilirsiniz:
@GetMapping("/export/csv")
public ResponseEntity<StreamingResponseBody> exportCsv() {
StreamingResponseBody stream = outputStream -> {
Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream));
writer.write("id,name,email\n");
// Veritabanından chunk chunk okuyarak yaz
int page = 0;
Page<User> users;
do {
users = userRepository.findAll(PageRequest.of(page, 1000));
for (User user : users.getContent()) {
writer.write(user.getId() + "," + user.getName() + "," + user.getEmail() + "\n");
}
writer.flush();
page++;
} while (users.hasNext());
writer.close();
};
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("text/csv"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=users.csv")
.body(stream);
}Bu yaklaşım, milyonlarca satır veriyi memory'yi tüketmeden döndürebilir. StreamingResponseBody veriyi parça parça (chunk) olarak HTTP yanıtına yazar.
ResponseEntity ile Asenkron İşlemler
Uzun süren işlemler için 202 Accepted kullanarak istemciye "isteğini aldım, işliyorum" mesajı verebilirsiniz:
@PostMapping("/reports/generate")
public ResponseEntity<Map<String, String>> generateReport(
@RequestBody ReportRequest request) {
String jobId = UUID.randomUUID().toString();
// İşlemi arka plana at
reportService.generateAsync(jobId, request);
// Hemen 202 Accepted döndür
return ResponseEntity.accepted()
.header("Location", "/api/reports/status/" + jobId)
.body(Map.of(
"jobId", jobId,
"status", "PROCESSING",
"checkStatusUrl", "/api/reports/status/" + jobId
));
}
@GetMapping("/reports/status/{jobId}")
public ResponseEntity<?> checkReportStatus(@PathVariable String jobId) {
ReportJob job = reportService.getJobStatus(jobId);
return switch (job.getStatus()) {
case PROCESSING -> ResponseEntity.ok(Map.of(
"status", "PROCESSING",
"progress", job.getProgress() + "%"
));
case COMPLETED -> ResponseEntity.ok()
.header("Location", "/api/reports/download/" + jobId)
.body(Map.of(
"status", "COMPLETED",
"downloadUrl", "/api/reports/download/" + jobId
));
case FAILED -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"status", "FAILED",
"error", job.getErrorMessage()
));
};
}Bu pattern, uzun süren işlemleri (rapor üretme, toplu veri aktarma, video dönüştürme) yönetmek için standarttır. İstemci job ID ile durumu sorgulayabilir.
Yaygın Hatalar
Hata 1: ResponseEntity<Void> ile body döndürmek
// ❌ YANLIŞ — Void tipinde body döndürmeye çalışmak
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.ok().body(null); // Gereksiz ve kafa karıştırıcı
}
// ✅ DOĞRU — noContent() kullanın
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build(); // 204 No Content
}Hata 2: Exception durumlarında ResponseEntity döndürmeye çalışmak
// ❌ YANLIŞ — Controller'da her hatayı yakalamak
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
try {
UserDto user = userService.findById(id);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
// ✅ DOĞRU — Exception'ları @ControllerAdvice'a bırakın
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
UserDto user = userService.findById(id); // Exception fırlatırsa Advice yakalar
return ResponseEntity.ok(user);
}Özet
`ResponseEntity` ile HTTP yanıtının status kodunu, header'larını ve gövdesini tam olarak kontrol edin. Sadece veri değil, bütün HTTP semantiğini taşıyın.
Static factory metodlarını (
ok(),created(),notFound(),noContent()) tercih edin — daha okunabilir ve daha az hata-prone.Generic response wrapper ile tutarlı API yanıtları sağlayın. İstemcilerin her yanıtı aynı formatta parse etmesini kolaylaştırın.
Doğru HTTP status kodlarını kullanın: 201 Created (POST), 204 No Content (DELETE), 404 Not Found, 409 Conflict.
`@RestControllerAdvice` ile global hata yönetimi yapın. Her exception türü için uygun ResponseEntity döndürün.
Cache-Control, ETag gibi header'lar ile performansı artırın ve gereksiz veri transferini azaltın.
AI Asistan
Sorularını yanıtlamaya hazır