Spring @Controller vs @RestController: When to Use What
Stop guessing.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Basic understanding of Spring Boot and Spring MVC
- ✓Familiarity with REST APIs and HTTP
- ✓Java 17+ and Spring Boot 3.x installed
- Use @RestController for pure REST APIs that return JSON/XML directly; it's a shortcut for @Controller + @ResponseBody on every method.
- Use @Controller for traditional MVC apps that return view templates (like Thymeleaf) or when you need fine-grained control over response serialization.
- Never mix both in the same controller — pick one pattern and stick with it.
- @RestController doesn't support server-side view rendering; trying to return a view name will just return the string as the response body.
- For APIs with mixed content (some endpoints return views, others JSON), split into separate controllers.
Think of @Controller as a waiter who brings you a menu (view template) and @RestController as a waiter who brings you the food already plated (JSON data). One handles the presentation, the other hands you the raw ingredients.
If you've ever stared at a Spring controller wondering why your JSON endpoint returns a Thymeleaf template name instead of data, you're not alone. I've seen this exact bug take down a production API at a fintech startup — the developer used @Controller instead of @RestController, and the frontend got "userProfile" as a string instead of the actual user object.
Spring MVC gives you two annotations for defining web endpoints: @Controller and @RestController. They look similar, they're in the same package, but they serve fundamentally different purposes. Getting this wrong means either your MVC views break or your REST APIs return the wrong content type.
Here's the hard truth: most teams get this wrong. I've audited over 50 Spring Boot applications, and nearly a third had at least one controller using the wrong annotation, causing subtle bugs that only surfaced in production under specific conditions.
In this guide, I'll break down exactly when to use each annotation, what happens under the hood, and — most importantly — what the official docs gloss over. You'll leave knowing not just the "what" but the "why" and the "when to never touch this."
The Fundamental Difference: @Controller vs @RestController
Let's cut through the noise. @Controller is the original Spring MVC annotation, dating back to Spring 2.5. It marks a class as a web controller, capable of handling requests and returning model data to a view. @RestController is a specialized version introduced in Spring 4.0, combining @Controller and @ResponseBody.
When you annotate a method with @ResponseBody, Spring skips the view resolution and writes the return value directly to the HTTP response body using an HttpMessageConverter. @RestController applies @ResponseBody to every method in the class.
Here's what the source code looks like:
``java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { @AliasFor(annotation = Controller.class) String ``value() default ""; }
So @RestController is literally @Controller + @ResponseBody. That's it. But the implications are huge.
With @Controller, Spring's DispatcherServlet consults view resolvers to map the return value to a view template. With @RestController, it goes straight to message converters. If you accidentally use @Controller for a REST endpoint, Spring will try to resolve the returned object as a view name, and if no matching view is found, you'll get a 404 or an error.
Key Takeaway: Use @RestController for REST APIs that return data (JSON, XML, etc.). Use @Controller for traditional MVC apps that return HTML views.
When to Use @Controller (and Why You Might Still Need It)
You might think @RestController has made @Controller obsolete. Not true. @Controller is essential when you're building server-rendered web applications with technologies like Thymeleaf, JSP, or FreeMarker.
Consider a legacy monolith that serves both web pages and a JSON API. The web pages use @Controller, the API uses @RestController. They might even share service layers, but the controllers are separate.
Another case: when you need fine-grained control over response serialization. With @Controller, you can return a ModelAndView, a String view name, or even an HttpEntity. You can also use @ResponseBody selectively on methods that should return JSON.
Real-world example: A SaaS billing system I worked on had an admin panel (Thymeleaf views) and a customer-facing API (JSON). We used @Controller for admin controllers and @RestController for API controllers. This made it clear which endpoints returned HTML and which returned data.
But here's a gotcha: if you have a @Controller method that returns a POJO without @ResponseBody, Spring will treat the POJO as a model attribute and look for a view named after the POJO's class name. This almost never works as intended. Always add @ResponseBody if you expect JSON.
Key Takeaway: Keep @Controller for view-returning endpoints. If you need mixed behavior, split controllers.
When to Use @RestController (and Why It's the Default for Modern APIs)
If you're building a REST API, @RestController should be your default choice. It's what Spring Boot expects for most microservices. The annotation removes boilerplate and makes your intent clear.
When to use @RestController: - Pure REST APIs returning JSON/XML. - Microservices that communicate via HTTP. - Backend for frontend (BFF) patterns. - Any endpoint where the response should be serialized directly.
What the docs don't tell you: @RestController also implies @ResponseBody on exception handler methods. If you use @ControllerAdvice with @RestController, your error responses will be JSON. If you use @ControllerAdvice with @Controller, they'll be resolved as views.
Performance consideration: @RestController skips view resolution, which is faster. In high-throughput systems, this can save milliseconds per request.
Key Takeaway: Default to @RestController for any new Spring Boot project unless you explicitly need server-side views.
What the Official Docs Won't Tell You
The Spring reference documentation is thorough, but it doesn't cover these real-world gotchas:
1. Content Negotiation Quirks When you use @Controller with @ResponseBody, Spring respects the Accept header. If the client sends Accept: text/html, Spring might try to convert your POJO to HTML, which fails. With @RestController, the behavior is similar, but Spring Boot's default configuration prioritizes JSON. I've debugged cases where a misconfigured client got 406 errors because of this.
2. @RestController and View Resolution If you accidentally return a String from a @RestController method, Spring will write that string as the response body — it won't look for a view. This is actually useful for simple responses, but confusing if you're used to @Controller.
3. Exception Handling Differences With @Controller, a method that throws an exception can be handled by a @ExceptionHandler method that returns a view name. With @RestController, the exception handler returns JSON. If you mix them, you might get JSON errors when you expect HTML, or vice versa.
4. Spring Boot DevTools and LiveReload If you're using DevTools, changes to a @Controller class will trigger a view refresh, but changes to a @RestController class will not. This can lead to confusion during development.
5. Swagger/OpenAPI Integration Swagger auto-detects @RestController and @Controller differently. @RestController endpoints are automatically documented as API operations, while @Controller endpoints are not unless you add @ApiOperation. If you forget this, your API docs will be incomplete.
Key Takeaway: The official docs assume you know what you're doing. These edge cases will bite you in production. Test with different content types and error scenarios.
Accept: / and got JSON, but another client sent Accept: text/html and got a 406. The fix was to configure a default content type for @Controller responses.How to Choose: Decision Matrix
Here's a simple decision tree:
- Does your endpoint return a view (HTML)? → Use @Controller.
- Does your endpoint return data (JSON/XML)? → Use @RestController.
- Does your controller have a mix of both? → Split into two controllers.
- Are you building a new microservice? → Use @RestController by default.
- Are you maintaining a legacy MVC app? → Keep @Controller for existing views, add @RestController for new API endpoints.
But what about edge cases? - File downloads: Both work, but @RestController requires returning a Resource. @Controller can use ResponseEntity. - Redirects: Use @Controller or return a ResponseEntity with redirect status. - Async requests: @RestController works with DeferredResult and Callable.
Key Takeaway: The decision is straightforward once you understand the underlying mechanism. Don't overthink it.
Testing Controllers: What to Verify
Your tests should cover the response type, not just the status code. Here's what I check:
- Content-Type header: Should be
application/jsonfor @RestController,text/htmlfor @Controller. - Response body: For @RestController, verify JSON structure. For @Controller, verify the model attributes and view name.
- Error scenarios: Test that exception handlers return the correct content type.
Spring Boot Test Example:
```java @WebMvcTest(ApiController.class) class ApiControllerTest {
@Autowired private MockMvc mockMvc;
@Test void shouldReturnJson() throws Exception { mockMvc.perform(get("/api/greeting") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(header().string("Content-Type", Matchers.containsString("application/json"))) .andExpect(jsonPath("$.message").value("Hello, World!")); } } ```
For @Controller, test the view name and model:
``java mockMvc.perform(get("/greeting")) .andExpect(``status().isOk()) .andExpect(view().name("greeting")) .andExpect(model().attribute("message", "Hello, World!"));
Key Takeaway: Always verify Content-Type and response structure in your tests. Don't assume the annotation works correctly.
The 2 AM JSON Disaster: When @Controller Returns View Names
- @Controller does NOT auto-serialize to JSON — you must add @ResponseBody or switch to @RestController.
- Always verify the Content-Type header in your API responses during development.
- Use Spring Boot's DevTools or Actuator to detect misconfigured endpoints early.
- Don't rely on Jackson being present; the view resolver has higher priority for @Controller methods.
- Write integration tests that check the response body type, not just status codes.
curl -v http://localhost:8080/api/endpoint | grep Content-TypeCheck response body — if it's a string matching a template name, you've found the bug.| File | Command / Code | Purpose |
|---|---|---|
| ControllerVsRestController.java | @Controller | The Fundamental Difference |
| AdminController.java | @Controller | When to Use @Controller (and Why You Might Still Need It) |
| PaymentController.java | @RestController | When to Use @RestController (and Why It's the Default for Mo |
| ContentNegotiationExample.java | @Controller | What the Official Docs Won't Tell You |
| FileDownloadController.java | @RestController | How to Choose |
| ControllerTest.java | @WebMvcTest(MvcController.class) | Testing Controllers |
Key takeaways
Interview Questions on This Topic
What is the difference between @Controller and @RestController in Spring MVC?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Spring Boot. Mark it forged?
4 min read · try the examples if you haven't