Spring Data REST
Giriş — Repository'den Otomatik API
Diyelim ki yeni bir projeye başlıyorsun. Bir admin paneli lazım, hızlıca CRUD endpoint'leri oluşturman gerekiyor. Entity, repository, service, controller, DTO, mapper... Hepsi standart, hepsi tekrarlayan boilerplate kod. Sadece 5 entity için bile 20+ dosya.
Peki ya sana şunu söylesem: tek bir dependency ekle, sıfır controller yaz, tüm CRUD endpoint'lerin hazır olsun?
Spring Data REST tam olarak bunu yapıyor. JPA repository'lerini otomatik olarak RESTful API'ye çeviriyor. HAL formatında, HATEOAS uyumlu, paging ve sorting dahil.
// Bu tek interface ile...
public interface ProductRepository extends JpaRepository<Product, Long> {
}
// Bu endpoint'lerin HEPSI otomatik oluşur:
// GET /products → Tüm ürünleri listele (paged)
// GET /products/1 → Tek ürün getir
// POST /products → Yeni ürün oluştur
// PUT /products/1 → Ürünü tamamen güncelle
// PATCH /products/1 → Ürünü kısmen güncelle
// DELETE /products/1 → Ürünü silSıfır controller. Sıfır service. Sıfır DTO. Sadece entity ve repository.
⚠️ Dikkat: Bu güç tehlikeli de olabilir. Tüm entity field'ları expose olur, business logic atlabilir, API contract kontrolden çıkabilir. Bu dersin sonunda ne zaman kullanılmalı, ne zaman kullanılmamalı detaylıca tartışacağız.
Kurulum
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<!-- JPA zaten varsa eklemeye gerek yok, yoksa: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Veritabanı (H2 ile test) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Temel Konfigürasyon
# application.properties
# Base path (varsayılan: /)
spring.data.rest.base-path=/api
# Sayfa boyutu (varsayılan: 20)
spring.data.rest.default-page-size=10
# Maksimum sayfa boyutu
spring.data.rest.max-page-size=100
# Sayfa parametresi adı (varsayılan: page)
spring.data.rest.page-param-name=page
# Boyut parametresi adı (varsayılan: size)
spring.data.rest.size-param-name=size
# Sıralama parametresi adı (varsayılan: sort)
spring.data.rest.sort-param-name=sort
# HAL explorer (browser'da API keşfi)
# spring-boot-starter-data-rest ile otomatik gelirMinimal Örnek
@Entity
@Table(name = "products")
@Getter @Setter @NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String description;
@Column(nullable = false)
private BigDecimal price;
private Integer stockQuantity;
@Column(nullable = false)
private String category;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
// Bu kadar! Başka hiçbir şey yazmana gerek yok.
public interface ProductRepository extends JpaRepository<Product, Long> {
}Uygulamayı çalıştır ve http://localhost:8080/api adresine git:
{
"_links": {
"products": {
"href": "http://localhost:8080/api/products{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/api/profile"
}
}
}Tebrikler, API'n hazır. Tek satır controller yazmadan.
@RepositoryRestResource — Endpoint Özelleştirme
Varsayılan davranışı özelleştirmek için @RepositoryRestResource kullan:
// Path ve collection name özelleştirme
@RepositoryRestResource(
path = "urunler", // /api/urunler (varsayılan: /api/products)
collectionResourceRel = "urunler", // JSON'daki _embedded key'i
itemResourceRel = "urun" // Tekil resource key'i
)
public interface ProductRepository extends JpaRepository<Product, Long> {
}
// Endpoint'i tamamen gizleme
@RepositoryRestResource(exported = false) // Bu repository API'de görünmez
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
}Belirli metotları gizleme:
public interface UserRepository extends JpaRepository<User, Long> {
// DELETE endpoint'ini devre dışı bırak
@Override
@RestResource(exported = false)
void deleteById(Long id);
// Tüm silme işlemlerini devre dışı bırak
@Override
@RestResource(exported = false)
void delete(User entity);
// Save'i de gizleyebilirsin (sadece read-only API)
@Override
@RestResource(exported = false)
<S extends User> S save(S entity);
}💡 İpucu:
exported = falseile hassas operasyonları gizleyebilirsin. Örneğin kullanıcı entity'sinde DELETE'i kapatıp, silme işlemini custom controller ile kontrollü yapabilirsin.
Otomatik CRUD — HAL Response Formatı
Spring Data REST, HAL (Hypertext Application Language) formatında yanıt döner. HAL, REST API'lerde HATEOAS prensibini uygular — her yanıtta ilgili kaynağa ulaşmak için linkler vardır.
POST — Yeni Kayıt Oluşturma
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "MacBook Pro 14",
"description": "M3 Pro chip, 18GB RAM",
"price": 74999.00,
"stockQuantity": 50,
"category": "Elektronik"
}'Response (201 Created):
{
"name": "MacBook Pro 14",
"description": "M3 Pro chip, 18GB RAM",
"price": 74999.00,
"stockQuantity": 50,
"category": "Elektronik",
"createdAt": "2025-01-15T14:30:00",
"updatedAt": null,
"_links": {
"self": {
"href": "http://localhost:8080/api/products/1"
},
"product": {
"href": "http://localhost:8080/api/products/1"
}
}
}GET — Tek Kayıt
curl http://localhost:8080/api/products/1GET — Liste (Paged)
curl "http://localhost:8080/api/products?page=0&size=3&sort=price,desc"Response:
{
"_embedded": {
"products": [
{
"name": "MacBook Pro 14",
"price": 74999.00,
"_links": {
"self": { "href": "http://localhost:8080/api/products/1" }
}
},
{
"name": "iPhone 15 Pro",
"price": 64999.00,
"_links": {
"self": { "href": "http://localhost:8080/api/products/2" }
}
}
]
},
"_links": {
"self": { "href": "http://localhost:8080/api/products?page=0&size=3&sort=price,desc" },
"next": { "href": "http://localhost:8080/api/products?page=1&size=3&sort=price,desc" },
"last": { "href": "http://localhost:8080/api/products?page=3&size=3&sort=price,desc" }
},
"page": {
"size": 3,
"totalElements": 10,
"totalPages": 4,
"number": 0
}
}PUT vs PATCH
# PUT — Tüm entity'yi güncelle (verilmeyen field'lar null olur!)
curl -X PUT http://localhost:8080/api/products/1 \
-H "Content-Type: application/json" \
-d '{
"name": "MacBook Pro 14 M3",
"price": 79999.00,
"stockQuantity": 45,
"category": "Elektronik"
}'
# description verilmedi → null olur!
# PATCH — Sadece verilen field'ları güncelle (diğerleri korunur)
curl -X PATCH http://localhost:8080/api/products/1 \
-H "Content-Type: application/json" \
-d '{
"price": 69999.00,
"stockQuantity": 30
}'
# Sadece price ve stockQuantity değişir, diğerleri aynı kalır⚠️ Dikkat:
PUTtüm entity'yi replace eder. Verilmeyen field'larnullolur. Kısmi güncelleme için her zaman `PATCH` kullan.
DELETE
curl -X DELETE http://localhost:8080/api/products/1
# 204 No ContentHAL Formatı Detayları
HAL response'unda 3 ana bölüm var:
{
"name": "MacBook Pro", // 1. Resource data (entity field'ları)
"price": 74999.00,
"_links": { // 2. Links (HATEOAS navigasyon)
"self": {
"href": "http://localhost:8080/api/products/1"
},
"product": {
"href": "http://localhost:8080/api/products/1"
},
"category": { // İlişkili kaynak linki
"href": "http://localhost:8080/api/products/1/category"
}
},
"_embedded": { // 3. Embedded resources (iç içe kaynaklar)
"category": {
"name": "Elektronik",
"_links": {
"self": { "href": "http://localhost:8080/api/categories/1" }
}
}
}
}HATEOAS'ın faydası: Client, URL'leri hardcode etmez. Her yanıttaki _links'i takip eder. API URL yapısı değişirse client'ın kodu bozulmaz — çünkü URL'leri dinamik olarak alıyor.
İlişkili Entity'ler
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Customer customer; // İlişki
@OneToMany(mappedBy = "order")
private List<OrderItem> items; // İlişki
private BigDecimal total;
private String status;
}
public interface OrderRepository extends JpaRepository<Order, Long> {}
public interface CustomerRepository extends JpaRepository<Customer, Long> {}# Order getirdiğinde customer linkini görürsün
curl http://localhost:8080/api/orders/1{
"total": 149.90,
"status": "COMPLETED",
"_links": {
"self": { "href": "http://localhost:8080/api/orders/1" },
"customer": { "href": "http://localhost:8080/api/orders/1/customer" },
"items": { "href": "http://localhost:8080/api/orders/1/items" }
}
}# İlişkili customer'ı getir
curl http://localhost:8080/api/orders/1/customer
# İlişkili item'ları getir
curl http://localhost:8080/api/orders/1/itemsİlişki kurma (association):
# Order'a customer ata (URI ile)
curl -X PUT http://localhost:8080/api/orders/1/customer \
-H "Content-Type: text/uri-list" \
-d "http://localhost:8080/api/customers/5"💡 İpucu: İlişki kurma
text/uri-listcontent type'ı kullanır — JSON değil. Bu Spring Data REST'e özgü bir yöntemdir ve alışılmadık gelebilir.
Projections — Custom Response Shape
Entity'nin tüm field'larını expose etmek istemeyebilirsin. Veya farklı endpoint'lerde farklı görünüm isteyebilirsin. Projection tam olarak bunu sağlar.
// Sadece temel bilgileri gösteren projection
@Projection(name = "summary", types = {Product.class})
public interface ProductSummary {
String getName();
BigDecimal getPrice();
String getCategory();
}
// Detaylı bilgileri gösteren projection
@Projection(name = "detail", types = {Product.class})
public interface ProductDetail {
String getName();
String getDescription();
BigDecimal getPrice();
Integer getStockQuantity();
String getCategory();
LocalDateTime getCreatedAt();
// Computed field — entity'de olmayan alan
@Value("#{target.price.multiply(new java.math.BigDecimal('1.20'))}")
BigDecimal getPriceWithTax();
}
// İlişkili entity'yi inline gösteren projection
@Projection(name = "withCategory", types = {Product.class})
public interface ProductWithCategory {
String getName();
BigDecimal getPrice();
CategoryInfo getCategory(); // nested projection
interface CategoryInfo {
String getName();
String getDescription();
}
}Kullanım:
# Varsayılan (tüm field'lar)
curl http://localhost:8080/api/products/1
# Summary projection
curl "http://localhost:8080/api/products/1?projection=summary"
# Detail projection (KDV dahil fiyat ile)
curl "http://localhost:8080/api/products/1?projection=detail"
# Liste endpoint'inde projection
curl "http://localhost:8080/api/products?projection=summary"Summary projection yanıtı:
{
"name": "MacBook Pro 14",
"price": 74999.00,
"category": "Elektronik",
"_links": {
"self": { "href": "http://localhost:8080/api/products/1" }
}
}⚠️ Dikkat: Projection, entity'nin field'larını gizlemez — sadece farklı bir görünüm sunar. Varsayılan endpoint (
?projectionparametresi olmadan) hâlâ tüm field'ları döner. Güvenlik için projection'a güvenme, field'ları entity seviyesinde koru.
Excerpts — Liste Endpoint'lerinde Projection
Normal liste endpoint'inde (/api/products) entity'lerin tüm field'ları döner. Excerpt ile liste endpoint'inde otomatik olarak bir projection uygulanmasını sağlayabilirsin:
@RepositoryRestResource(excerptProjection = ProductSummary.class)
public interface ProductRepository extends JpaRepository<Product, Long> {
}Artık /api/products listesinde her ürün ProductSummary projection'ı ile döner. Ama /api/products/1 (tekil) hâlâ tüm field'ları döner.
# Liste — otomatik excerpt (summary) uygulanır
curl http://localhost:8080/api/products
# Tekil — tüm field'lar (excerpt uygulanmaz)
curl http://localhost:8080/api/products/1
# Tekilde de projection istersen parametreyle
curl "http://localhost:8080/api/products/1?projection=summary"💡 İpucu: Excerpt, özellikle liste endpoint'lerinde performans için faydalı. 50 ürün listelediğinde description gibi büyük text field'ları taşımak gereksiz — excerpt ile sadece name, price, category taşırsın.
Event Hooks — Business Logic Ekleme
Spring Data REST, CRUD işlemlerinden önce ve sonra event'ler fırlatır. Bu event'leri yakalayarak business logic ekleyebilirsin:
@Component
@RepositoryEventHandler
public class ProductEventHandler {
private static final Logger log = LoggerFactory.getLogger(ProductEventHandler.class);
// CREATE öncesi
@HandleBeforeCreate
public void handleBeforeCreate(Product product) {
log.info("Creating product: {}", product.getName());
// Varsayılan değerler ata
if (product.getStockQuantity() == null) {
product.setStockQuantity(0);
}
// İsim normalizasyonu
product.setName(product.getName().trim());
// Fiyat kontrolü
if (product.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Fiyat sıfırdan büyük olmalı");
}
}
// CREATE sonrası
@HandleAfterCreate
public void handleAfterCreate(Product product) {
log.info("Product created: id={}, name={}", product.getId(), product.getName());
// Bildirim gönder, cache temizle, audit log yaz...
}
// UPDATE öncesi
@HandleBeforeSave
public void handleBeforeSave(Product product) {
log.info("Updating product: id={}", product.getId());
product.setUpdatedAt(LocalDateTime.now());
// Stok negatife düşmesin
if (product.getStockQuantity() != null && product.getStockQuantity() < 0) {
throw new IllegalArgumentException("Stok negatif olamaz");
}
}
// UPDATE sonrası
@HandleAfterSave
public void handleAfterSave(Product product) {
log.info("Product updated: id={}", product.getId());
}
// DELETE öncesi
@HandleBeforeDelete
public void handleBeforeDelete(Product product) {
log.warn("Deleting product: id={}, name={}", product.getId(), product.getName());
// Aktif siparişi olan ürün silinemesin
// (Bu tür kontroller için genelde service katmanı daha uygun)
}
// DELETE sonrası
@HandleAfterDelete
public void handleAfterDelete(Product product) {
log.info("Product deleted: id={}", product.getId());
}
}Event lifecycle:
POST /api/products
│
├─ @HandleBeforeCreate ← Validation, default değerler
├─ repository.save() ← JPA persist
└─ @HandleAfterCreate ← Bildirim, audit log
PUT/PATCH /api/products/1
│
├─ @HandleBeforeSave ← Validation, güncelleme kontrolleri
├─ repository.save() ← JPA merge
└─ @HandleAfterSave ← Cache temizleme
DELETE /api/products/1
│
├─ @HandleBeforeDelete ← Silme izni kontrolü
├─ repository.delete() ← JPA remove
└─ @HandleAfterDelete ← Temizlik⚠️ Dikkat: Event handler'larda fırlatılan exception'lar 500 Internal Server Error döner. Daha anlamlı hata mesajları için
ResponseEntityExceptionHandlerveya@ControllerAdvicekullan.
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(Map.of("error", e.getMessage()));
}
}Search Endpoints — Otomatik Query Expose
Repository'deki derived query method'ları otomatik olarak search endpoint'i haline gelir:
public interface ProductRepository extends JpaRepository<Product, Long> {
// GET /api/products/search/findByCategory?category=Elektronik
List<Product> findByCategory(String category);
// GET /api/products/search/findByPriceBetween?min=100&max=500
List<Product> findByPriceBetween(
@Param("min") BigDecimal min,
@Param("max") BigDecimal max
);
// GET /api/products/search/findByNameContainingIgnoreCase?name=mac
List<Product> findByNameContainingIgnoreCase(@Param("name") String name);
// GET /api/products/search/findByCategoryAndPriceLessThan?category=Elektronik&price=50000
Page<Product> findByCategoryAndPriceLessThan(
@Param("category") String category,
@Param("price") BigDecimal price,
Pageable pageable
);
// Belirli search endpoint'ini gizle
@Override
@RestResource(exported = false)
List<Product> findAll();
// Endpoint adını özelleştir
@RestResource(path = "ucuz-urunler", rel = "ucuz-urunler")
List<Product> findByPriceLessThan(@Param("maxPrice") BigDecimal maxPrice);
}Search endpoint'lerini keşfetme:
# Tüm arama endpoint'lerini listele
curl http://localhost:8080/api/products/search{
"_links": {
"findByCategory": {
"href": "http://localhost:8080/api/products/search/findByCategory{?category}",
"templated": true
},
"findByPriceBetween": {
"href": "http://localhost:8080/api/products/search/findByPriceBetween{?min,max}",
"templated": true
},
"findByNameContainingIgnoreCase": {
"href": "http://localhost:8080/api/products/search/findByNameContainingIgnoreCase{?name}",
"templated": true
},
"ucuz-urunler": {
"href": "http://localhost:8080/api/products/search/ucuz-urunler{?maxPrice}",
"templated": true
}
}
}Kullanım:
# Kategoriye göre ara
curl "http://localhost:8080/api/products/search/findByCategory?category=Elektronik"
# Fiyat aralığı
curl "http://localhost:8080/api/products/search/findByPriceBetween?min=1000&max=5000"
# İsim araması (case-insensitive)
curl "http://localhost:8080/api/products/search/findByNameContainingIgnoreCase?name=mac"
# Paging ile arama
curl "http://localhost:8080/api/products/search/findByCategoryAndPriceLessThan?category=Elektronik&price=50000&page=0&size=5&sort=price,asc"💡 İpucu:
@Paramannotation'ı URL query parametresi adını belirler. Kullanmazsan parametrelerarg0,arg1gibi çirkin isimlerle expose olur.
Paging ve Sorting
Spring Data REST, tüm liste endpoint'lerinde otomatik paging ve sorting desteği sunar:
# Sayfalama
curl "http://localhost:8080/api/products?page=0&size=5"
# Sıralama (artan)
curl "http://localhost:8080/api/products?sort=price,asc"
# Sıralama (azalan)
curl "http://localhost:8080/api/products?sort=price,desc"
# Çoklu sıralama
curl "http://localhost:8080/api/products?sort=category,asc&sort=price,desc"
# Hepsi birlikte
curl "http://localhost:8080/api/products?page=0&size=10&sort=category,asc&sort=price,desc"Response'daki page bilgisi:
{
"_embedded": { ... },
"_links": {
"first": { "href": "...?page=0&size=5" },
"self": { "href": "...?page=2&size=5" },
"next": { "href": "...?page=3&size=5" },
"prev": { "href": "...?page=1&size=5" },
"last": { "href": "...?page=9&size=5" }
},
"page": {
"size": 5,
"totalElements": 47,
"totalPages": 10,
"number": 2
}
}Client tarafında pagination yapmak için _links.next, _links.prev linklerini takip etmen yeterli. URL yapısını bilmeye gerek yok — HATEOAS'ın gücü bu.
Validators — Repository Event Öncesi Doğrulama
Spring Data REST, JSR-303 (Bean Validation) ile entegre çalışır:
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Ürün adı boş olamaz")
@Size(min = 2, max = 200, message = "Ürün adı 2-200 karakter olmalı")
private String name;
@NotNull(message = "Fiyat zorunlu")
@DecimalMin(value = "0.01", message = "Fiyat 0.01'den büyük olmalı")
private BigDecimal price;
@Min(value = 0, message = "Stok negatif olamaz")
private Integer stockQuantity;
@NotBlank(message = "Kategori zorunlu")
private String category;
}Ama bu yetmezse, custom validator da ekleyebilirsin:
@Component("beforeCreateProductValidator")
public class ProductValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Product.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Product product = (Product) target;
// Kategori kontrolü
List<String> validCategories = List.of("Elektronik", "Giyim", "Gıda", "Kitap");
if (!validCategories.contains(product.getCategory())) {
errors.rejectValue("category", "invalid.category",
"Geçersiz kategori. Kabul edilen: " + validCategories);
}
// İş kuralı: Gıda kategorisinde max fiyat 10.000 TL
if ("Gıda".equals(product.getCategory()) &&
product.getPrice().compareTo(new BigDecimal("10000")) > 0) {
errors.rejectValue("price", "price.too.high",
"Gıda kategorisinde fiyat 10.000 TL'yi geçemez");
}
}
}// Validator'ı Spring Data REST'e kaydet
@Configuration
public class RestValidationConfig implements RepositoryRestConfigurer {
private final Validator productValidator;
public RestValidationConfig(
@Qualifier("beforeCreateProductValidator") Validator productValidator) {
this.productValidator = productValidator;
}
@Override
public void configureValidatingRepositoryEventListener(
ValidatingRepositoryEventListener listener) {
listener.addValidator("beforeCreate", productValidator);
listener.addValidator("beforeSave", productValidator);
}
}Validation hatası response'u:
{
"errors": [
{
"entity": "Product",
"property": "category",
"invalidValue": "Mobilya",
"message": "Geçersiz kategori. Kabul edilen: [Elektronik, Giyim, Gıda, Kitap]"
}
]
}Custom Controller Override
Belirli endpoint'leri elle yazmak isteyebilirsin — özellikle business logic gerektiren işlemler için:
@RepositoryRestController // @RestController DEĞİL!
@RequestMapping("/api/products")
public class ProductCustomController {
private final ProductRepository productRepository;
private final InventoryService inventoryService;
public ProductCustomController(ProductRepository productRepository,
InventoryService inventoryService) {
this.productRepository = productRepository;
this.inventoryService = inventoryService;
}
// Bu endpoint Spring Data REST'in otomatik endpoint'ini OVERRIDE eder
@GetMapping("/{id}")
public ResponseEntity<EntityModel<Product>> getProduct(@PathVariable Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
// Ekstra business logic: görüntülenme sayısını artır
product.incrementViewCount();
productRepository.save(product);
EntityModel<Product> model = EntityModel.of(product);
model.add(linkTo(methodOn(ProductCustomController.class).getProduct(id)).withSelfRel());
return ResponseEntity.ok(model);
}
// Custom endpoint — stok güncelleme
@PatchMapping("/{id}/stock")
public ResponseEntity<EntityModel<Product>> updateStock(
@PathVariable Long id,
@RequestBody StockUpdateRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
inventoryService.updateStock(product, request.getQuantity(), request.getReason());
return ResponseEntity.ok(EntityModel.of(product));
}
}⚠️ Dikkat: Override controller'da
@RepositoryRestControllerkullan,@RestControllerdeğil.@RestControllerkullanırsan Spring Data REST'in endpoint'i ile çakışır ve beklenmedik davranışlar olur.@RepositoryRestControlleraynı base-path altında çalışır ve Spring Data REST endpoint'ini override eder.
// ❌ YANLIŞ — Spring Data REST ile çakışır
@RestController
@RequestMapping("/api/products")
public class ProductController { }
// ✅ DOĞRU — Spring Data REST endpoint'ini override eder
@RepositoryRestController
@RequestMapping("/api/products")
public class ProductController { }CORS Konfigürasyonu
Frontend uygulamalar farklı domain'den API'ye erişiyorsa CORS ayarı gerekir:
@Configuration
public class RestConfig implements RepositoryRestConfigurer {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config,
CorsRegistry cors) {
// Base path ayarla
config.setBasePath("/api");
// ID'leri response'a dahil et (varsayılan: dahil değil!)
config.exposeIdsFor(Product.class, Customer.class, Order.class);
// CORS
cors.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://myapp.com")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}⚠️ Dikkat: Spring Data REST varsayılan olarak entity ID'lerini response'a dahil etmez. Bu kafa karıştırıcı olabilir.
config.exposeIdsFor()ile görmek istediğin entity'leri belirt. Aksi halde client ID bilgisi alamaz ve güncelleme/silme yapamaz.
Tam Bir Örnek: Kitap Mağazası Admin API
Tüm kavramları bir araya getiren örnek:
Entity'ler
@Entity
@Table(name = "books")
@Getter @Setter @NoArgsConstructor
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String title;
@NotBlank
private String isbn;
@NotNull
@DecimalMin("0.01")
private BigDecimal price;
@Min(0)
private int stock;
private String description;
private int pageCount;
private LocalDate publishDate;
@ManyToOne
@JoinColumn(name = "publisher_id")
private Publisher publisher;
@ManyToMany
@JoinTable(
name = "book_authors",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "author_id")
)
private Set<Author> authors = new HashSet<>();
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
@Entity
@Table(name = "authors")
@Getter @Setter @NoArgsConstructor
public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String biography;
}
@Entity
@Table(name = "publishers")
@Getter @Setter @NoArgsConstructor
public class Publisher {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String country;
}Repository'ler
@RepositoryRestResource(excerptProjection = BookSummary.class)
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByTitleContainingIgnoreCase(@Param("title") String title);
Page<Book> findByPriceLessThan(@Param("maxPrice") BigDecimal maxPrice, Pageable pageable);
@RestResource(path = "by-isbn", rel = "by-isbn")
Book findByIsbn(@Param("isbn") String isbn);
List<Book> findByAuthorsNameContaining(@Param("authorName") String authorName);
@Query("SELECT b FROM Book b WHERE b.stock < :threshold")
@RestResource(path = "low-stock", rel = "low-stock")
List<Book> findLowStockBooks(@Param("threshold") int threshold);
}
@RepositoryRestResource
public interface AuthorRepository extends JpaRepository<Author, Long> {
List<Author> findByNameContainingIgnoreCase(@Param("name") String name);
}
@RepositoryRestResource
public interface PublisherRepository extends JpaRepository<Publisher, Long> {
}Projections
@Projection(name = "summary", types = {Book.class})
public interface BookSummary {
Long getId();
String getTitle();
String getIsbn();
BigDecimal getPrice();
int getStock();
}
@Projection(name = "detail", types = {Book.class})
public interface BookDetail {
String getTitle();
String getIsbn();
String getDescription();
BigDecimal getPrice();
int getStock();
int getPageCount();
LocalDate getPublishDate();
Set<AuthorSummary> getAuthors();
PublisherSummary getPublisher();
interface AuthorSummary {
String getName();
}
interface PublisherSummary {
String getName();
String getCountry();
}
}Event Handler
@Component
@RepositoryEventHandler
@Slf4j
public class BookEventHandler {
@HandleBeforeCreate
public void handleBeforeCreate(Book book) {
log.info("Creating book: {}", book.getTitle());
// ISBN format kontrolü
String isbn = book.getIsbn().replaceAll("-", "");
if (isbn.length() != 13 && isbn.length() != 10) {
throw new IllegalArgumentException("Geçersiz ISBN formatı");
}
}
@HandleAfterCreate
public void handleAfterCreate(Book book) {
log.info("Book created: id={}, title={}, isbn={}",
book.getId(), book.getTitle(), book.getIsbn());
}
@HandleBeforeDelete
public void handleBeforeDelete(Book book) {
if (book.getStock() > 0) {
throw new IllegalArgumentException(
"Stokta bulunan kitap silinemez. Mevcut stok: " + book.getStock());
}
}
}Konfigürasyon
@Configuration
public class DataRestConfig implements RepositoryRestConfigurer {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config,
CorsRegistry cors) {
config.setBasePath("/api");
config.exposeIdsFor(Book.class, Author.class, Publisher.class);
config.setDefaultPageSize(20);
config.setMaxPageSize(100);
cors.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE");
}
}API Kullanım Senaryoları
# 1. Tüm kitapları listele (excerpt projection ile)
curl http://localhost:8080/api/books
# 2. Detaylı kitap bilgisi
curl "http://localhost:8080/api/books/1?projection=detail"
# 3. ISBN ile ara
curl "http://localhost:8080/api/books/search/by-isbn?isbn=978-0-13-468599-1"
# 4. Yazar adına göre ara
curl "http://localhost:8080/api/books/search/findByAuthorsNameContaining?authorName=Martin"
# 5. Düşük stoklu kitaplar
curl "http://localhost:8080/api/books/search/low-stock?threshold=5"
# 6. Yeni kitap ekle
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{
"title": "Clean Code",
"isbn": "978-0-13-235088-4",
"price": 249.90,
"stock": 100,
"description": "A Handbook of Agile Software Craftsmanship",
"pageCount": 464,
"publishDate": "2008-08-01"
}'
# 7. Kitaba yazar ata
curl -X POST http://localhost:8080/api/books/1/authors \
-H "Content-Type: text/uri-list" \
-d "http://localhost:8080/api/authors/1"
# 8. Kitaba publisher ata
curl -X PUT http://localhost:8080/api/books/1/publisher \
-H "Content-Type: text/uri-list" \
-d "http://localhost:8080/api/publishers/1"
# 9. Fiyat güncelle (PATCH)
curl -X PATCH http://localhost:8080/api/books/1 \
-H "Content-Type: application/json" \
-d '{"price": 199.90}'
# 10. Ucuzdan pahalıya sırala, sayfa 2
curl "http://localhost:8080/api/books?sort=price,asc&page=1&size=10"Neden Production'da Dikkatli Olunmalı?
Spring Data REST çok güçlü bir araç. Ama büyük güç, büyük sorumluluk getirir. Production'da kullanırken farkında olman gereken riskler:
1. Tüm Entity Field'ları Expose Olur (Güvenlik Riski)
@Entity
public class User {
private Long id;
private String username;
private String email;
private String passwordHash; // 🚨 API'den erişilebilir!
private String tcKimlikNo; // 🚨 KVKK ihlali!
private BigDecimal salary; // 🚨 Hassas veri!
private String internalNotes; // 🚨 İç notlar!
}Repository'yi expose ettiğin anda, GET /api/users/1 ile tüm field'lar görünür. @JsonIgnore veya projection kullansan bile, birileri farklı yollarla erişebilir.
2. Complex Business Logic İçin Yetersiz
// Sipariş oluşturma business logic:
// 1. Stok kontrolü
// 2. Fiyat hesaplama (indirim, kampanya, KDV)
// 3. Ödeme işlemi
// 4. Stok düşürme
// 5. Email gönderme
// 6. Kargo oluşturma
// 7. Audit log
// Spring Data REST ile sadece "POST /api/orders" yapabilirsin
// Ama yukarıdaki 7 adımı nereye koyacaksın?
// Event handler'a mı? 7 adımlık logic event handler'a sığmaz.3. API Contract Değişikliğine Açık
// Entity'ye yeni field eklediğinde...
@Entity
public class Product {
private String name;
private BigDecimal price;
private String internalSku; // Yeni field → otomatik API'de görünür!
}
// API response'u değişti → Client'lar bozulabilir
// REST endpoint'te ise DTO kullandığın için bu kontrol altında4. Debugging Zorluğu
Bir hata olduğunda, Controller → Service → Repository akışını takip edebilirsin. Ama Spring Data REST'te controller yok — Spring'in internal mekanizmaları çalışıyor. Debug etmek zor, stack trace'ler karmaşık.
Ne Zaman Kullanılır?
| Senaryo | Spring Data REST? |
|---|---|
| Admin panel (internal tool) | ✅ Harika |
| Prototip / MVP | ✅ Hızlı başlangıç |
| Internal microservice (team içi) | ✅ Uygun |
| CRUD-heavy uygulama (basit domain) | ✅ İdeal |
| Hackathon / demo | ✅ Süper hızlı |
Ne Zaman Kullanılmaz?
| Senaryo | Spring Data REST? |
|---|---|
| Public API | ❌ Güvenlik, versiyonlama, rate limit eksik |
| Complex domain (e-ticaret, bankacılık) | ❌ Business logic katmanı lazım |
| Microservice boundary | ❌ API contract kontrolü önemli |
| Mobile BFF (Backend for Frontend) | ❌ Client'a özel response shape lazım |
| Güvenlik-kritik uygulama | ❌ Field-level kontrol yetersiz |
⚠️ Dikkat: Spring Data REST'i production'da public API olarak kullanma. Internal tool, admin panel veya prototip için mükemmel. Ama external client'ların erişeceği API'lerde controller + service + DTO kalıbını kullan. Güvenlik, validasyon ve business logic kontrolünü kaybetme.
Hibrit Yaklaşım (Önerilen)
// Internal/admin entity'ler → Spring Data REST
@RepositoryRestResource
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
// Otomatik CRUD — admin panel için yeterli
}
// Public/complex entity'ler → Klasik Controller + Service
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// Manuel kontrol — business logic, güvenlik, DTO mapping
}
// Hassas entity'ler → Export etme
@RepositoryRestResource(exported = false)
public interface UserCredentialRepository extends JpaRepository<UserCredential, Long> {
// API'de görünmez — sadece internal kullanım
}Bu hibrit yaklaşımda basit CRUD entity'leri (audit log, kategori, etiket gibi) Spring Data REST ile hızlıca expose edersin. Karmaşık iş mantığı gerektiren entity'leri (sipariş, ödeme, kullanıcı) ise klasik controller ile yazarsın.
Özet
Spring Data REST, JPA repository'lerini otomatik olarak RESTful endpoint'lere çevirir — sıfır controller, sıfır service, sıfır DTO ile tam CRUD API
HAL formatı ile HATEOAS uyumlu yanıtlar döner —
_linksile navigasyon,_embeddedile iç içe kaynaklar, otomatik paging ve sortingProjection ve excerpt ile response shape'ini özelleştir — farklı client'lar için farklı görünümler sun, gereksiz field'ları gizle
Event hooks (
@HandleBeforeCreate,@HandleAfterSave) ile business logic ekle — validation, audit log, bildirim. Ama karmaşık iş mantığı için yetersiz kalırProduction'da dikkatli ol — tüm entity field'ları expose olabilir, API contract kontrolden çıkabilir, debugging zorlaşır. Güvenlik-kritik ve karmaşık domain'lerde kullanma
İdeal kullanım alanları: admin panel, internal tool, prototip, basit CRUD. Public API, e-ticaret, bankacılık gibi karmaşık domain'lerde klasik controller + service + DTO kalıbını tercih et
AI Asistan
Sorularını yanıtlamaya hazır