REST vs. MVC in Spring

When to use @RestController versus traditional server-rendered views, and how content negotiation decides the representation.

Two different jobs under one framework

@Controller and @RestController both run through the exact same DispatcherServlet/HandlerMapping machinery (see the Spring MVC introduction page) — the difference is entirely about what happens to a handler method's return value, and that difference maps onto two genuinely different kinds of application.

Server-rendered views (@Controller) REST API (@RestController)
Return value A logical view name, resolved to a template The actual response body
Client A browser, navigating between full pages Any HTTP client — a SPA, a mobile app, another service
State Often session-based (logged-in user, flash messages) Typically stateless — every request is self-contained
Response format HTML JSON (or XML), consumed programmatically
Typical use case An internal admin tool, a server-rendered marketing site, a traditional multi-page app A backend consumed by a separate frontend, or by other services

When traditional server-rendered MVC is still the right call

Not every application needs a decoupled frontend. A @Controller returning Thymeleaf-rendered HTML is often simpler — no separate frontend build, no CORS to configure, no client-side routing — and is a reasonable default for:

  • Internal tools and admin panels where development speed matters more than a rich client-side experience.
  • Applications with modest interactivity needs, where full-page navigation is an acceptable user experience.
  • Teams without dedicated frontend expertise, where one Spring Boot app handling both the UI and the data is the whole team's stack.

When @RestController is the right call

A REST API is the right shape when the client and the server are genuinely separate concerns — a JavaScript SPA (React/Vue/Angular), a native mobile app, or another backend service consuming the API. In these cases, HTML rendering is either irrelevant (a mobile client) or someone else's job entirely (a separately built and deployed frontend), and the Spring application's only job is to serve data.

Content negotiation: choosing a representation

A single endpoint can serve more than one representation of the same resource, letting the client's Accept header decide which one it gets back:

Java
@RestController
@RequestMapping("/api/books")
public class BookController {

    @GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
    public Book getBook(@PathVariable Long id) {
        return bookService.findById(id);
        // Jackson serializes to JSON for "Accept: application/json",
        // and (with the XML module on the classpath) to XML for "Accept: application/xml"
    }
}
Bash
curl -H "Accept: application/json" localhost:8080/api/books/1
curl -H "Accept: application/xml"  localhost:8080/api/books/1

In practice, the overwhelming majority of modern APIs serve JSON exclusively and never declare more than one produces value — content negotiation across multiple formats is much more common historically (SOAP-adjacent APIs, some enterprise integrations) than in typical greenfield REST APIs today. It's still worth understanding, since it's exactly the same mechanism that decides how Spring resolves a request's Accept: text/html in a mixed application that serves both a REST API and server-rendered pages from the same DispatcherServlet.

Mixing both styles in one application

Nothing prevents a single Spring Boot application from having both kinds of controllers — an admin UI rendered with Thymeleaf, alongside a JSON API consumed by a mobile app:

Java
@Controller
@RequestMapping("/admin/books")
public class AdminBookPageController {
    // returns view names -- renders Thymeleaf templates
}

@RestController
@RequestMapping("/api/books")
public class BookApiController {
    // returns data -- serialized to JSON
}

Keeping them as clearly separate controller classes (rather than mixing @ResponseBody-annotated and view-returning methods inside the same class) keeps each controller's contract obvious at a glance.

Common mistakes

  • Building a REST API "because it's more modern" for an application that's really just an internal tool with no separate frontend — server-rendered MVC is often simpler and perfectly appropriate there.
  • Mixing view-returning and JSON-returning methods inside the same controller class, forcing every reader to check each method's annotations individually to know what it actually returns.
  • Declaring produces for multiple formats without actually needing to support more than one — added complexity (and testing surface) for a capability no real client uses.
  • Assuming content negotiation is only relevant for exotic multi-format APIs — the same negotiation machinery decides ordinary Accept: application/json handling too; it just usually has only one real option to pick from.