@RestController
Marks a class as a web controller whose method return values are written straight to the response body, usually as JSON.
@RestController("optionalBeanName")- Added in Spring Boot Spring Framework 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | String= "" | The bean name. Leave it empty to get the decapitalised class name. |
Return value
n/aRegisters the class as a Spring MVC controller bean and applies @ResponseBody to every handler method, so return values go through the HTTP message converters (Jackson for JSON) instead of view resolution.
Errors
Startup fails with ConflictingBeanDefinitionException if two controllers end up with the same bean name.
@RestController is the annotation you put on every class that serves a JSON API. It is a shortcut for two annotations: @Controller, which makes the class a component that Spring MVC scans for handler methods, and @ResponseBody, which tells Spring to write each method's return value into the response body using a message converter. With Jackson on the classpath (it comes with spring-boot-starter-webmvc), records, lists and maps become JSON.
Because it is a stereotype, the class is picked up by component scanning, so it must live in the package of your @SpringBootApplication class or below it. A controller in a sibling package is silently ignored and every request to it returns 404.
Why it matters
With plain @Controller a returned String is a view name. In an API with no template engine that turns into a confusing error about a missing view. @RestController removes the choice: a String becomes a text/plain body, an object becomes JSON. Use @Controller only when the class renders server-side templates.
Keep controllers thin
A controller is the HTTP edge of the application. It maps paths, binds input, calls a service and shapes the response. Business rules, transactions and repository calls belong in services. Controllers that grow into hundreds of lines are hard to test with @WebMvcTest because the slice no longer loads what they depend on.
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductCatalogue catalogue; // injected through the constructor
...
}Mistakes people make
- Returning a JPA entity directly. Lazy relations and internal fields leak into the JSON, and any change to the table changes your API. Return a record DTO.
- Two classes with the same simple name in different packages. Both get the same default bean name and startup fails. Give one an explicit name with the
valueattribute. - Field injection with
@Autowired. Constructor injection keeps the dependency final and lets a unit test build the controller without Spring.
The annotation has a single attribute, the bean name. Everything else (paths, methods, media types) is configured with the mapping annotations on the class and its methods.
Example
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
record ProductView(long id, String name, int priceInPence) {}
class ProductCatalogue {
List<ProductView> all() {
return List.of(new ProductView(1, "Kettle", 2499), new ProductView(2, "Toaster", 3199));
}
}
("/api/products")
public class ProductController {
private final ProductCatalogue catalogue;
ProductController(ProductCatalogue catalogue) {
this.catalogue = catalogue;
}
List<ProductView> list() {
// The list is written straight to the response as a JSON array.
return catalogue.all();
}
}
When to use it
- Exposing a product catalogue as JSON to a single-page front end
- Writing an internal service API that other back ends call over HTTP
- Providing a small status or version endpoint that returns a JSON map
Frequently asked questions
Do I still need @ResponseBody on methods?
No. @RestController is meta-annotated with @ResponseBody, so every handler method in the class already writes its return value to the body.
Can a @RestController return HTML?
It can return an HTML string, but it will not resolve templates. For server-side views use @Controller with a template engine such as Thymeleaf.
Why does my controller return 404 for every path?
Usually it is outside the package tree of the @SpringBootApplication class, so component scanning never registers it.
More examples
What @RestController replaces
This is the long form: @Controller plus @ResponseBody on the method. Forgetting @ResponseBody on one method of a @Controller is a classic bug; @RestController applies it to every method.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class StatusController {
// Without @ResponseBody, "ok" would be treated as a view name
// and the request would fail looking for a template called "ok".
@GetMapping("/status")
@ResponseBody
String status() {
return "ok";
}
}
Naming the controller bean
The value attribute is the bean name. You only need it when two controllers share a simple class name, for example a public and an admin OrderController.
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
// Two classes called OrderController in different packages would both
// default to the bean name "orderController" and startup would fail.
@RestController("adminOrderController")
public class OrderController {
@GetMapping("/admin/orders/summary")
Map<String, Integer> summary() {
return Map.of("open", 12, "shipped", 340);
}
}
Testing the controller in a web slice
@WebMvcTest starts only the MVC layer for the named controller. MockMvcTester sends a request without a real server and AssertJ checks the status and the JSON field.
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class PingController {
@GetMapping("/api/ping")
Map<String, String> ping() {
return Map.of("status", "up");
}
}
@WebMvcTest(PingController.class)
class PingControllerTest {
@Autowired
MockMvcTester mvc;
@Test
void writesJson() {
assertThat(mvc.get().uri("/api/ping"))
.hasStatusOk()
.bodyJson()
.extractingPath("$.status").isEqualTo("up");
}
}
Discussion