@RestController vs @Controller
Spring MVC'de HTTP isteklerini karşılayan iki temel annotation vardır: @Controller ve @RestController. Bu iki annotation, Spring Boot ile web geliştirmenin temelini oluşturur ve aralarındaki farkı kavramak, doğru mimari kararlar vermenizi sağlar.
Bir an için bir restoranı düşünün. Mutfakta iki tür şef var: biri yemeği tabağa güzelce dizip, dekorasyonuyla birlikte (HTML sayfası) sunan sunum şefi (@Controller), diğeri ise sadece malzeme listesini (JSON verisi) çıkaran hazırlık şefi (@RestController). İkisi de aynı mutfağı kullanır, ama çıktıları farklıdır.
En basit ifadeyle:
@RestController = @Controller + @ResponseBodyBu eşitlik çok şey anlatır. @Controller geleneksel MVC uygulamalarında view (template) döndürmek için kullanılırken, @RestController REST API geliştirmede doğrudan veri (JSON/XML) döndürmek için kullanılır.
@Controller — Template Rendering
@Controller annotation'ı ile işaretlenen sınıflar, genellikle bir view adı (template name) döndürür. Spring MVC, bu view adını ViewResolver aracılığıyla bir HTML template dosyasına eşleştirir ve bu template'i render ederek kullanıcıya HTML yanıtı gönderir.
Bu mekanizma, geleneksel Server-Side Rendering (SSR) yaklaşımıdır. Sunucu tarafında HTML üretilir ve tarayıcıya gönderilir. Tarayıcı sadece bu HTML'i gösterir — ekstra JavaScript çalıştırmasına gerek yoktur.
@Controller
public class WebPageController {
private final UserService userService;
public WebPageController(UserService userService) {
this.userService = userService;
}
@GetMapping("/home")
public String homePage(Model model) {
model.addAttribute("title", "Ana Sayfa");
model.addAttribute("message", "Hoş Geldiniz!");
return "home"; // → templates/home.html (Thymeleaf)
}
@GetMapping("/users")
public String userList(Model model) {
List<User> users = userService.findAll();
model.addAttribute("users", users);
return "user-list"; // → templates/user-list.html
}
@GetMapping("/users/{id}")
public String userDetail(@PathVariable Long id, Model model) {
User user = userService.findById(id);
model.addAttribute("user", user);
model.addAttribute("orders", user.getOrders());
return "user-detail"; // → templates/user-detail.html
}
}Burada Model nesnesi, controller'dan view'a veri aktarmak için kullanılır. model.addAttribute() ile eklenen veriler, template içinde kullanılabilir hale gelir. Bu mekanizma, bir bavulun içine eşya koyup başka birine teslim etmeye benzer — controller bavulu doldurur, template bavulu açıp eşyaları yerleştirir.
Thymeleaf template'i şöyle görünür:
<!-- templates/home.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${title}">Default Title</title>
</head>
<body>
<h1 th:text="${message}">Default Message</h1>
<table>
<tr th:each="user : ${users}">
<td th:text="${user.name}">İsim</td>
<td th:text="${user.email}">Email</td>
<td>
<a th:href="@{/users/{id}(id=${user.id})}">Detay</a>
</td>
</tr>
</table>
</body>
</html>View Resolution Süreci
Spring MVC'de bir controller metodu String döndürdüğünde, bu String doğrudan tarayıcıya gönderilmez. ViewResolver zinciri devreye girer:
return "home"
↓
ViewResolver: prefix + viewName + suffix
↓
"classpath:/templates/" + "home" + ".html"
↓
templates/home.html dosyası bulunur
↓
Template engine (Thymeleaf) dosyayı render eder
↓
Oluşan HTML tarayıcıya gönderilirSpring Boot'ta Thymeleaf varsayılan yapılandırması classpath:/templates/ prefix'i ve .html suffix'ini kullanır. Bu değerleri application.properties'de değiştirebilirsiniz:
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false # Geliştirme sırasında cache kapatınModel, ModelMap ve ModelAndView
Veri aktarımı için birkaç yol vardır:
@Controller
public class DataPassingController {
// 1. Model interface (önerilen)
@GetMapping("/v1")
public String withModel(Model model) {
model.addAttribute("data", "Hello");
return "page";
}
// 2. ModelMap (Model'in implementasyonu)
@GetMapping("/v2")
public String withModelMap(ModelMap modelMap) {
modelMap.addAttribute("data", "Hello");
return "page";
}
// 3. ModelAndView (view adı ve veri birlikte)
@GetMapping("/v3")
public ModelAndView withModelAndView() {
ModelAndView mav = new ModelAndView("page");
mav.addObject("data", "Hello");
return mav;
}
// 4. @ModelAttribute ile (tüm metotlar için ortak veri)
@ModelAttribute("appName")
public String appName() {
return "My Application"; // Her view'da ${appName} kullanılabilir
}
}💡 İpucu:
Modelinterface'i en temiz yaklaşımdır.ModelAndViewise view adı ve veriyi tek nesnede taşımak istediğinizde kullanışlıdır. PratikteModelkullanımı daha yaygın ve okunabilirdir.
@ResponseBody — JSON Döndürme
Peki ya bir controller metodundan view değil, doğrudan veri döndürmek isterseniz? İşte burada @ResponseBody devreye girer. Bu annotation, Spring'e "dönüş değerini view adı olarak yorumlama, doğrudan HTTP yanıt gövdesine yaz" der.
@Controller
public class ApiController {
private final UserService userService;
public ApiController(UserService userService) {
this.userService = userService;
}
@GetMapping("/api/users")
@ResponseBody // ← Bu annotation olmasaydı, Spring "users" adlı template arardı
public List<User> getUsers() {
return userService.findAll();
// Jackson otomatik olarak JSON'a çevirir
}
@GetMapping("/api/users/{id}")
@ResponseBody
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
@GetMapping("/api/status")
@ResponseBody
public Map<String, String> getStatus() {
return Map.of(
"status", "UP",
"version", "1.2.3",
"timestamp", LocalDateTime.now().toString()
);
}
}@ResponseBody kullanıldığında Spring, dönüş değerini HttpMessageConverter aracılığıyla uygun formata çevirir. Spring Boot'ta Jackson kütüphanesi otomatik olarak yapılandırıldığı için, Java nesneleri otomatik olarak JSON'a serialize edilir.
HttpMessageConverter Zinciri
Spring MVC, @ResponseBody ile döndürülen nesneleri HTTP yanıtına dönüştürmek için converter zinciri kullanır. İstemcinin Accept header'ına göre uygun converter seçilir:
Java Nesnesi → HttpMessageConverter → HTTP Yanıt Gövdesi
MappingJackson2HttpMessageConverter → application/json
MappingJackson2XmlHttpMessageConverter → application/xml
StringHttpMessageConverter → text/plain
ByteArrayHttpMessageConverter → application/octet-streamSpring Boot, Jackson'ı spring-boot-starter-web ile otomatik yapılandırır. Hiçbir ek ayar yapmadan JSON serializasyonu çalışır.
@RestController — @Controller + @ResponseBody
Her metoda tek tek @ResponseBody yazmak zahmetlidir. REST API geliştirirken neredeyse her metot veri döndürür. İşte bu yüzden Spring 4.0'da @RestController eklendi:
// @RestController = sınıf seviyesinde @Controller + @ResponseBody
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> getAllProducts() {
return productService.findAll();
// @ResponseBody'ye gerek yok — @RestController zaten sağlıyor
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productService.save(product);
}
@PutMapping("/{id}")
public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
return productService.update(id, product);
}
@DeleteMapping("/{id}")
public void deleteProduct(@PathVariable Long id) {
productService.delete(id);
}
}@RestController'ın kaynak koduna bakarsanız, gerçekten de iki annotation'ın birleşimi olduğunu görürsünüz:
// Spring Framework kaynak kodu
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}Bu meta-annotation yapısı, Spring'in annotation composition mekanizmasının güzel bir örneğidir.
Thymeleaf (SSR) vs REST API Yaklaşımı — Derinlemesine Karşılaştırma
Modern web uygulamaları iki temel mimari yaklaşımla geliştirilebilir. Her birinin güçlü ve zayıf yönleri vardır.
1. Server-Side Rendering (SSR) — @Controller + Thymeleaf
Sunucu HTML'i oluşturur ve tarayıcıya gönderir. Tarayıcı sadece HTML'i gösterir.
Kullanıcı → GET /products → Controller → Model + View Name
↓
ViewResolver → Thymeleaf
↓
HTML oluşturulur
↓
← HTML yanıtAvantajları:
SEO dostu: Arama motorları HTML'i doğrudan okuyabilir
İlk yükleme hızlı: JavaScript bundle indirip çalıştırmaya gerek yok
Basit mimari: Frontend-backend arası API katmanı yok
CSRF koruması kolay: Form token'ları otomatik
Dezavantajları:
Her sayfa değişikliğinde tam sayfa yenilenir (full page reload)
Mobil uygulama desteği zor — HTML mobilde kullanışsız
Zengin kullanıcı etkileşimleri (drag & drop, gerçek zamanlı güncelleme) zor
Frontend ve backend geliştiricileri aynı projede çalışmak zorunda
2. REST API + SPA — @RestController
Sunucu sadece JSON verisi döndürür. Frontend (React, Angular, Vue.js) bu veriyi alır ve arayüzü oluşturur.
Kullanıcı → React App → GET /api/products → RestController → JSON yanıt
↓
React veriyi alır
↓
DOM güncellenir (sanal DOM)Avantajları:
Tek API, çoklu istemci: Web, mobil, masaüstü aynı API'yi kullanır
Zengin kullanıcı deneyimi: SPA ile sayfa yenileme yok, anlık tepki
Takım ayrımı: Frontend ve backend bağımsız geliştirilir
Ölçeklenebilirlik: API katmanı bağımsız ölçeklenir
Dezavantajları:
İlk yükleme daha yavaş (JavaScript bundle)
SEO ek çaba gerektirir (SSR, pre-rendering)
İki ayrı proje yönetimi
CORS yapılandırması gerekli
⚠️ Dikkat: SSR vs SPA seçimi projenin ihtiyaçlarına bağlıdır. "Her zaman REST API kullan" diye bir kural yoktur. Blog siteleri için Thymeleaf mükemmelken, interaktif bir dashboard için React + REST API daha uygundur.
Karar Tablosu
| Senaryo | Tercih | Neden |
|---|---|---|
| Blog sitesi, kurumsal web | @Controller + Thymeleaf | SEO önemli, basit etkileşim |
| Admin paneli | Her ikisi de olabilir | Karmaşıklığa göre karar ver |
| SPA (React/Vue/Angular) backend | @RestController | Frontend ayrı, sadece JSON gerekli |
| Mobil uygulama backend | @RestController | Mobil uygulamalar JSON bekler |
| Microservice API | @RestController | Servisler arası iletişim JSON/gRPC |
| Hem web hem mobil destekli | @RestController | Tek API, çoklu istemci |
| İç araçlar, hızlı prototip | @Controller + Thymeleaf | Hızlı geliştirme, ekstra frontend yok |
| E-ticaret sitesi | Hibrit veya SPA | SEO + zengin UX ihtiyacı |
Karma Kullanım — Aynı Projede Her İkisi
Bazı projelerde hem template rendering hem de REST API gerekebilir. Örneğin, bir e-ticaret sitesi düşünün: admin paneli Thymeleaf ile server-side render edilirken, ürün arama ve filtreleme mobil uygulama için REST API sunar.
// Web sayfaları için — @Controller
@Controller
@RequestMapping("/web")
public class WebController {
private final DashboardService dashboardService;
public WebController(DashboardService dashboardService) {
this.dashboardService = dashboardService;
}
@GetMapping("/dashboard")
public String dashboard(Model model) {
model.addAttribute("stats", dashboardService.getStats());
model.addAttribute("recentOrders", dashboardService.getRecentOrders(10));
return "dashboard"; // HTML template döner
}
@GetMapping("/reports")
public String reports(Model model) {
model.addAttribute("monthlyReport", dashboardService.getMonthlyReport());
return "reports"; // HTML template döner
}
}
// REST API için — @RestController
@RestController
@RequestMapping("/api/v1")
public class ApiController {
private final DashboardService dashboardService;
public ApiController(DashboardService dashboardService) {
this.dashboardService = dashboardService;
}
@GetMapping("/stats")
public DashboardStats getStats() {
return dashboardService.getStats(); // JSON döner
}
@GetMapping("/orders/recent")
public List<OrderDto> getRecentOrders(@RequestParam(defaultValue = "10") int limit) {
return dashboardService.getRecentOrders(limit); // JSON döner
}
}💡 İpucu: Karma kullanımda URL yapısını net tutun:
/web/**web sayfaları için,/api/**REST API için. Bu ayırım, CORS ve güvenlik yapılandırmasını da kolaylaştırır.
@Controller ile JSON Döndürmenin Diğer Yolları
@Controller sınıfı içinde bazı metodların JSON, bazılarının view döndürmesini istiyorsanız ResponseEntity kullanabilirsiniz:
@Controller
public class HybridController {
@GetMapping("/page")
public String renderPage(Model model) {
model.addAttribute("title", "Sayfa Başlığı");
return "my-page"; // Template döndürür
}
@GetMapping("/data")
public ResponseEntity<Map<String, Object>> getData() {
Map<String, Object> data = Map.of(
"key", "value",
"count", 42,
"items", List.of("a", "b", "c")
);
return ResponseEntity.ok(data); // JSON döndürür — @ResponseBody gerekmez
}
@GetMapping("/download")
public ResponseEntity<byte[]> downloadFile() {
byte[] content = fileService.getFileContent("report.pdf");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "report.pdf");
return new ResponseEntity<>(content, headers, HttpStatus.OK);
}
}ResponseEntity döndürdüğünüzde Spring otomatik olarak bunu HTTP yanıt gövdesine yazar — @ResponseBody gerekmez. Çünkü ResponseEntity zaten HTTP yanıtının tamamını (status code + headers + body) temsil eder.
Spring MVC Request İşleme Akışı
Her iki annotation'ın arka planda nasıl çalıştığını anlamak için, Spring MVC'nin istek işleme akışını bilmek önemlidir:
HTTP İsteği
↓
DispatcherServlet (Front Controller)
↓
HandlerMapping → Uygun Controller metodu bulunur
↓
HandlerAdapter → Metot çağrılır
↓
┌──────────────────────────────┐
│ @Controller │
│ return "viewName" │
│ ↓ │
│ ViewResolver → Template │
│ ↓ │
│ Template Engine → HTML │
│ ↓ │
│ HTML Response │
└──────────────────────────────┘
┌──────────────────────────────┐
│ @RestController │
│ return JavaObject │
│ ↓ │
│ HttpMessageConverter │
│ ↓ │
│ JSON/XML Response │
└──────────────────────────────┘DispatcherServlet, Spring MVC'nin kalbidir — tüm HTTP isteklerini karşılar ve uygun handler'a yönlendirir. @Controller kullanıldığında ViewResolver devreye girer; @RestController kullanıldığında HttpMessageConverter devreye girer.
Gerçek Dünya Senaryosu: E-Ticaret Uygulaması
Bir e-ticaret uygulamasında hem web arayüzü hem de API katmanı gerekir. İşte tam bir örnek:
// DTO — Her iki controller'da da kullanılır
public record ProductDto(
Long id,
String name,
String description,
BigDecimal price,
String imageUrl,
int stockCount
) {
public static ProductDto fromEntity(Product product) {
return new ProductDto(
product.getId(),
product.getName(),
product.getDescription(),
product.getPrice(),
product.getImageUrl(),
product.getStockCount()
);
}
}
// Web Controller — SSR
@Controller
@RequestMapping("/shop")
public class ShopWebController {
private final ProductService productService;
private final CategoryService categoryService;
public ShopWebController(ProductService productService,
CategoryService categoryService) {
this.productService = productService;
this.categoryService = categoryService;
}
@GetMapping
public String shopPage(Model model,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page) {
Page<ProductDto> products = productService.findByCategory(category, page, 20);
model.addAttribute("products", products);
model.addAttribute("categories", categoryService.findAll());
model.addAttribute("selectedCategory", category);
return "shop"; // templates/shop.html
}
@GetMapping("/product/{slug}")
public String productDetail(@PathVariable String slug, Model model) {
ProductDto product = productService.findBySlug(slug);
model.addAttribute("product", product);
model.addAttribute("relatedProducts", productService.findRelated(product.id(), 4));
return "product-detail";
}
}
// REST API Controller — Mobil + SPA
@RestController
@RequestMapping("/api/v1/products")
public class ProductApiController {
private final ProductService productService;
public ProductApiController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<Page<ProductDto>> listProducts(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Page<ProductDto> products = productService.findByCategory(category, page, size);
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
public ResponseEntity<ProductDto> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<ProductDto> createProduct(@RequestBody @Valid CreateProductRequest request) {
ProductDto created = productService.create(request);
URI location = URI.create("/api/v1/products/" + created.id());
return ResponseEntity.created(location).body(created);
}
}Bu örnekte aynı ProductService hem web controller hem de API controller tarafından kullanılır. İş mantığı service katmanında, sunum mantığı controller'larda ayrılmıştır.
Yaygın Hatalar ve Çözümleri
Hata 1: @Controller ile JSON Döndürmeye Çalışmak
// ❌ YANLIŞ — "userList" adında template arar ve 500 hatası verir
@Controller
public class UserController {
@GetMapping("/api/users")
public List<User> getUsers() {
return userService.findAll(); // Template resolver hatası!
}
}
// ✅ DOĞRU — @ResponseBody ekleyin veya @RestController kullanın
@Controller
public class UserController {
@GetMapping("/api/users")
@ResponseBody
public List<User> getUsers() {
return userService.findAll(); // JSON döner
}
}Hata 2: @RestController ile Template Döndürmeye Çalışmak
// ❌ YANLIŞ — "home" stringini doğrudan JSON olarak döner: "home"
@RestController
public class PageController {
@GetMapping("/home")
public String homePage() {
return "home"; // Template render edilmez, "home" stringi döner!
}
}
// ✅ DOĞRU — HTML sayfaları için @Controller kullanın
@Controller
public class PageController {
@GetMapping("/home")
public String homePage(Model model) {
return "home"; // templates/home.html render edilir
}
}Hata 3: İki Controller Türünü Aynı Sınıfta Karıştırmak
// ❌ YANLIŞ — @RestController tüm metotlara @ResponseBody uygular
@RestController
public class MixedController {
@GetMapping("/page")
public String page() {
return "my-page"; // "my-page" stringi JSON olarak döner, template değil!
}
@GetMapping("/data")
public User data() {
return userService.findById(1L); // JSON olarak döner ✓
}
}
// ✅ DOĞRU — İki ayrı sınıf kullanın
@Controller
public class WebController {
@GetMapping("/page")
public String page(Model model) { return "my-page"; }
}
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/data")
public User data() { return userService.findById(1L); }
}⚠️ Dikkat: Bir sınıfta
@RestControllerkullanıyorsanız, o sınıftaki HİÇBİR metot template döndüremez. Template ve JSON'ı karıştırmanız gereken nadir durumlarda, iki ayrı controller sınıfı oluşturmak en temiz yaklaşımdır.
@RestController ile Unit Test
@RestController'lar genellikle MockMvc ile test edilir:
@WebMvcTest(ProductApiController.class)
class ProductApiControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void shouldReturnProductList() throws Exception {
List<ProductDto> products = List.of(
new ProductDto(1L, "Laptop", "Desc", BigDecimal.valueOf(999.99), null, 10)
);
when(productService.findByCategory(any(), anyInt(), anyInt()))
.thenReturn(new PageImpl<>(products));
mockMvc.perform(get("/api/v1/products"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].name").value("Laptop"))
.andExpect(jsonPath("$.content[0].price").value(999.99));
}
@Test
void shouldReturn404WhenProductNotFound() throws Exception {
when(productService.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/v1/products/999"))
.andExpect(status().isNotFound());
}
}Performans ve Best Practice'ler
DTO Kullanın: Entity'leri doğrudan döndürmeyin. Entity'ler JPA proxy'leri, lazy-loading koleksiyonları ve veritabanı şemasına bağımlılık içerir. DTO'lar API sözleşmenizi entity değişikliklerinden korur.
ResponseEntity Tercih Edin:
@RestControllerile bileResponseEntitykullanmak, status kodları ve header'lar üzerinde kontrol sağlar.API Versiyonlama: REST API'lerde URL'de versiyon kullanın (
/api/v1/products). Bu, eski istemcileri bozmadan API'yi geliştirebilmenizi sağlar.Paket Yapısı: Web controller'ları ve API controller'ları ayrı paketlerde tutun:
`` com.example.app ├── web/ # @Controller sınıfları │ ├── ShopWebController │ └── AdminWebController └── api/ # @RestController sınıfları ├── ProductApiController └── UserApiController ``
Özet
`@Controller`, HTML template (Thymeleaf, Freemarker) render eder. Model ile view'a veri aktarılır. SSR uygulamaları için kullanılır.
`@RestController`,
@Controller+@ResponseBodykısayoludur. JSON/XML veri döndürür. REST API geliştirmek için kullanılır.`@ResponseBody`, controller metodunun dönüş değerini doğrudan HTTP yanıt gövdesine yazar.
@RestControllersınıflarında otomatik uygulanır.Karma kullanım gerektiğinde, web ve API controller'ları ayrı sınıflarda tanımlayın. URL prefix'lerini (
/web/**,/api/**) net tutun.Modern projelerde REST API + SPA yaklaşımı baskındır, ancak SEO gereksinimleri veya hızlı prototipleme için SSR hâlâ güçlü bir seçenektir.
Karar verirken: Tek bir istemci (web) → SSR düşünün. Çoklu istemci (web + mobil + 3. parti) → REST API zorunlu.
AI Asistan
Sorularını yanıtlamaya hazır