Home Java Spring @Controller vs @RestController: When to Use What
Beginner 4 min · July 14, 2026

Spring @Controller vs @RestController: When to Use What

Stop guessing.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic understanding of Spring Boot and Spring MVC
  • Familiarity with REST APIs and HTTP
  • Java 17+ and Spring Boot 3.x installed
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Spring @Controller and @RestController Annotations?

Spring @Controller and @RestController are annotations that mark Java classes as web controllers, with @RestController being a convenience that automatically serializes return values to HTTP responses.

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).
Plain-English First

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.

``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.

ControllerVsRestController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class MvcController {
    @GetMapping("/greeting")
    public String greeting(Model model) {
        model.addAttribute("message", "Hello, World!");
        return "greeting"; // resolves to greeting.html via Thymeleaf
    }
}

@RestController
public class ApiController {
    @GetMapping("/api/greeting")
    public Greeting greeting() {
        return new Greeting("Hello, World!"); // serialized to JSON
    }
}
Output
MVC: returns HTML page with message "Hello, World!"
REST: returns JSON: {"message":"Hello, World!"}
⚠ Don't Mix Patterns in One Controller
📊 Production Insight
In Spring Boot 2.x, the default content negotiation favors JSON when Jackson is present. But with @Controller, the view resolver still takes precedence for string return types. This is a common trap.
🎯 Key Takeaway
@RestController is just @Controller with @ResponseBody on every method. Choose based on whether you're returning views or data.

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.

AdminController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
@RequestMapping("/admin")
public class AdminController {

    @GetMapping("/users")
    public String listUsers(Model model) {
        List<User> users = userService.findAll();
        model.addAttribute("users", users);
        return "users/list"; // Thymeleaf template
    }

    @GetMapping("/users/{id}")
    @ResponseBody
    public User getUser(@PathVariable Long id) {
        return userService.findById(id); // JSON response for this method only
    }
}
Output
GET /admin/users -> renders users/list.html
GET /admin/users/1 -> returns JSON: {"id":1,"name":"John"}
💡Use @Controller for Error Pages Too
📊 Production Insight
In Spring Boot 3.x, the default view resolver (ContentNegotiatingViewResolver) can cause unexpected behavior if you have both JSON and HTML endpoints. Always test content negotiation with different Accept headers.
🎯 Key Takeaway
@Controller is still relevant for server-side rendering. Use it for Thymeleaf, JSP, or any view technology.

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.

PaymentController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    @PostMapping
    public PaymentResponse processPayment(@RequestBody PaymentRequest request) {
        // business logic
        return paymentService.process(request);
    }

    @ExceptionHandler(PaymentException.class)
    public ErrorResponse handlePaymentError(PaymentException e) {
        return new ErrorResponse(e.getCode(), e.getMessage());
    }
}
Output
POST /api/payments -> returns JSON: {"status":"success","transactionId":"abc123"}
On error: {"code":"INSUFFICIENT_FUNDS","message":"Balance too low"}
🔥@RestController and @ControllerAdvice
📊 Production Insight
I once saw a team use @RestController on a controller that returned a String for a redirect. The browser received the redirect URL as JSON instead of following it. They had to switch to @Controller and use RedirectView.
🎯 Key Takeaway
@RestController is the go-to for REST APIs. It's concise, fast, and integrates well with Spring Boot's auto-configuration.

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.

ContentNegotiationExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class ContentNegotiationController {

    @GetMapping("/data")
    @ResponseBody
    public MyData getData() {
        return new MyData("value");
    }
}

// Client sends: Accept: text/html
// Result: 406 Not Acceptable (unless you have an HTML message converter)
Output
With Accept: application/json -> {"field":"value"}
With Accept: text/html -> 406 error (or custom HTML if configured)
⚠ 406 Errors Are a Symptom of Misconfigured Content Negotiation
📊 Production Insight
In a production incident, a client sent 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.
🎯 Key Takeaway
The docs gloss over content negotiation, exception handling, and Swagger integration. Test these scenarios explicitly.

How to Choose: Decision Matrix

  1. Does your endpoint return a view (HTML)? → Use @Controller.
  2. Does your endpoint return data (JSON/XML)? → Use @RestController.
  3. Does your controller have a mix of both? → Split into two controllers.
  4. Are you building a new microservice? → Use @RestController by default.
  5. 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.

FileDownloadController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class FileDownloadController {

    @GetMapping("/download/{fileId}")
    public Resource downloadFile(@PathVariable String fileId) {
        // returns file as Resource
        return new FileSystemResource("/path/to/" + fileId);
    }
}

@Controller
public class RedirectController {

    @GetMapping("/old-page")
    public String redirect() {
        return "redirect:/new-page";
    }
}
Output
GET /download/123 -> returns file binary with Content-Disposition header
GET /old-page -> 302 redirect to /new-page
💡Use ResponseEntity for Maximum Control
📊 Production Insight
I've seen teams use @RestController for everything, then struggle when they need a redirect. Return ResponseEntity with redirect status instead of fighting the framework.
🎯 Key Takeaway
Use the decision matrix to pick the right annotation. When in doubt, prefer @RestController for APIs.

Testing Controllers: What to Verify

Your tests should cover the response type, not just the status code. Here's what I check:

  1. Content-Type header: Should be application/json for @RestController, text/html for @Controller.
  2. Response body: For @RestController, verify JSON structure. For @Controller, verify the model attributes and view name.
  3. 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!")); } } ```

``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.

ControllerTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebMvcTest(MvcController.class)
class MvcControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldReturnView() throws Exception {
        mockMvc.perform(get("/greeting"))
                .andExpect(status().isOk())
                .andExpect(view().name("greeting"))
                .andExpect(model().attribute("message", "Hello, World!"));
    }

    @Test
    void shouldReturnJsonForApi() throws Exception {
        mockMvc.perform(get("/api/greeting")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(header().string("Content-Type", 
                    Matchers.containsString("application/json")));
    }
}
Output
Both tests pass, verifying the correct response types.
🔥Test with Different Accept Headers
📊 Production Insight
I've seen tests pass with status 200 but fail in production because the response body was a view name string instead of JSON. Always verify the response structure.
🎯 Key Takeaway
Comprehensive tests should check Content-Type, view name, and model attributes to ensure your controller annotation is correct.
● Production incidentPOST-MORTEMseverity: high

The 2 AM JSON Disaster: When @Controller Returns View Names

Symptom
The frontend received a string like "paymentSuccess" instead of the expected JSON object with transaction details. Users saw raw text on the screen.
Assumption
The developer assumed @Controller automatically serializes objects to JSON when Jackson is on the classpath.
Root cause
The controller was annotated with @Controller but lacked @ResponseBody on the method. Spring's view resolver kicked in, treating the returned object as a view name.
Fix
Changed @Controller to @RestController on the class, which applies @ResponseBody to all methods automatically.
Key lesson
  • @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.
Production debug guideSymptom to Action4 entries
Symptom · 01
API returns HTML instead of JSON
Fix
Check if the controller is annotated with @Controller instead of @RestController. If it is, add @ResponseBody to the method or switch to @RestController.
Symptom · 02
JSON response contains unexpected fields or is empty
Fix
Verify Jackson serialization configuration. @RestController uses HttpMessageConverters; ensure Jackson is on the classpath and no custom converters interfere.
Symptom · 03
View template not rendering, returns raw string
Fix
If using @RestController, it cannot return view names. Use @Controller for view-returning endpoints, or return ModelAndView explicitly.
Symptom · 04
Some endpoints work, others don't
Fix
Check if individual methods have @ResponseBody while the class uses @Controller. This is fine, but inconsistent. Better to split into separate controllers.
★ Quick Debug Cheat SheetInstantly diagnose and fix the most common @Controller/@RestController issues.
REST endpoint returns view name string
Immediate action
Check if class has @Controller without @ResponseBody
Commands
curl -v http://localhost:8080/api/endpoint | grep Content-Type
Check response body — if it's a string matching a template name, you've found the bug.
Fix now
Replace @Controller with @RestController on the class.
MVC endpoint returns JSON instead of HTML+
Immediate action
Check if class has @RestController
Commands
Inspect the method return type — if it returns a view name, but class is @RestController, fix the annotation.
Look for @ResponseBody on method — remove it if you want a view.
Fix now
Change @RestController to @Controller and ensure method returns a String (view name) or ModelAndView.
Feature@Controller@RestController
Introduced inSpring 2.5Spring 4.0
Return typeView name (String) or ModelAndViewResponse body (POJO, String, etc.)
View resolutionYes, via ViewResolverNo, skipped
@ResponseBody needed?Yes, for data responsesNo, applied automatically
Exception handlingReturns view by defaultReturns JSON/XML by default
Typical use caseServer-rendered web appsREST APIs, microservices
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
ControllerVsRestController.java@ControllerThe Fundamental Difference
AdminController.java@ControllerWhen to Use @Controller (and Why You Might Still Need It)
PaymentController.java@RestControllerWhen to Use @RestController (and Why It's the Default for Mo
ContentNegotiationExample.java@ControllerWhat the Official Docs Won't Tell You
FileDownloadController.java@RestControllerHow to Choose
ControllerTest.java@WebMvcTest(MvcController.class)Testing Controllers

Key takeaways

1
@RestController is @Controller + @ResponseBody
use for REST APIs returning data.
2
@Controller is for server-side view rendering (Thymeleaf, JSP, etc.).
3
Never mix both patterns in the same controller; split into separate classes.
4
Always test Content-Type headers and response body structure in integration tests.
5
Be aware of content negotiation and exception handling differences between the two.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between @Controller and @RestController in Spring...
Q02SENIOR
How does content negotiation work with @Controller and @RestController?
Q03SENIOR
Can you explain a production issue you've seen with @Controller vs @Rest...
Q01 of 03JUNIOR

What is the difference between @Controller and @RestController in Spring MVC?

ANSWER
@Controller is used to define a controller in Spring MVC that typically returns a view (HTML). @RestController is a specialized version that combines @Controller and @ResponseBody, meaning every method returns the response body directly (usually JSON).
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can I use @Controller and @RestController together in the same class?
02
Does @RestController affect exception handling?
03
Can I return a view from a @RestController?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

4 min read · try the examples if you haven't

Previous
Spring REST Error Handling: Custom Error Messages and Exception Handling
95 / 121 · Spring Boot
Next
Spring @RequestBody and @ResponseBody: Request-Response Body Binding