@RestController

Marks a class as a web controller whose method return values are written straight to the response body, usually as JSON.

Signature
@RestController("optionalBeanName")
  • Added in Spring Boot Spring Framework 4.0

Parameters

ParameterTypeDescription
$valueoptionalString= ""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 value attribute.
  • 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

Example · java
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) {}

@Service
class ProductCatalogue {
    List<ProductView> all() {
        return List.of(new ProductView(1, "Kettle", 2499), new ProductView(2, "Toaster", 3199));
    }
}

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductCatalogue catalogue;

    ProductController(ProductCatalogue catalogue) {
        this.catalogue = catalogue;
    }

    @GetMapping
    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.

Example · java
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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.