Building REST APIs with Spring Boot

REST Principles & @RestController

18 min Lesson 1 of 13

REST Principles & @RestController

Before writing a single annotation, it is worth understanding what REST actually is and why it became the dominant style for web APIs. REST (Representational State Transfer) is an architectural style, not a protocol or a standard. It was defined by Roy Fielding in his 2000 doctoral dissertation as a set of constraints that, when applied to a distributed system, produce desirable properties: scalability, statelessness, uniform interfaces, and easy evolvability.

Spring Boot 3 and Spring 6 give you first-class tooling to build REST APIs, but the framework cannot enforce REST semantics for you — it just makes it easy to wire HTTP verbs, paths, and serialization together. Knowing the principles is what lets you design APIs that age well rather than ones that accumulate workarounds.

The Six REST Constraints

Fielding's original dissertation lists six constraints. Three are fundamental for everyday API design:

  1. Uniform Interface. Clients and servers interact through a consistent interface — resources identified by URIs, manipulated through representations, with self-descriptive messages and hypermedia links. In HTTP-based REST this translates to: use nouns for resource paths (/orders/42, not /getOrder?id=42), use the HTTP method to express the operation, and return meaningful status codes.
  2. Statelessness. Each request must contain all information needed to process it. The server stores no session state between requests. This is why REST APIs favour tokens in headers (Authorization: Bearer …) over server-side sessions — any instance in a cluster can handle any request.
  3. Client-Server Separation. The client manages the UI; the server manages data and business logic. They evolve independently as long as the API contract holds. This is the boundary @RestController sits on.

The remaining three (Caching, Layered System, Code-on-Demand) are important at scale but less relevant when you are first structuring your controllers.

Resources, Representations, and HTTP Verbs

A resource is any named concept your API exposes: a product, an order, a user, a report. Resources are identified by URIs. A representation is the current state of that resource serialised into a format the client can consume — usually JSON, occasionally XML or CSV.

HTTP verbs carry intent:

  • GET — retrieve a representation; must be safe (no side effects) and idempotent.
  • POST — create a new subordinate resource or trigger an action; neither safe nor idempotent.
  • PUT — replace a resource entirely; idempotent (calling it twice has the same result as calling it once).
  • PATCH — partial update; not necessarily idempotent.
  • DELETE — remove the resource; idempotent.
Idempotency matters for reliability. If a client does not receive a response (network timeout), it can safely retry an idempotent operation without worrying about duplicate side effects. Design PUT and DELETE to be idempotent; handle POST duplicates with client-supplied idempotency keys or unique constraints.

The @RestController Stereotype

In a Spring MVC application, @Controller marks a class as a web layer component and by default expects its methods to return view names. Adding @ResponseBody to a method tells Spring to write the return value directly into the HTTP response body instead of resolving a view.

@RestController is a composed annotation — it is literally @Controller + @ResponseBody applied at the class level. Every handler method in the class implicitly carries @ResponseBody. That is the only difference. The full annotation chain resolves to a Spring-managed bean in the application context, eligible for dependency injection, AOP advice, and the dispatcher servlet's request-mapping infrastructure.

package com.example.catalog.product; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController // @Controller + @ResponseBody @RequestMapping("/api/v1/products") // base path for all methods in this class public class ProductController { private final ProductService service; // constructor injection — preferred over @Autowired on a field public ProductController(ProductService service) { this.service = service; } @GetMapping // handles GET /api/v1/products public List<ProductDto> list() { return service.findAll(); } }

When Spring Boot's auto-configuration detects spring-boot-starter-web on the classpath it registers Jackson (MappingJackson2HttpMessageConverter) as the default message converter. Jackson serialises List<ProductDto> to JSON automatically — you never write ObjectMapper boilerplate by hand in a controller.

How the Request-Response Cycle Works

Understanding the path from an incoming HTTP request to the JSON response gives you better debugging instincts:

  1. The embedded Tomcat servlet container receives the TCP connection and parses the HTTP request.
  2. DispatcherServlet — the Spring MVC front controller — consults its HandlerMapping registry to find which controller method handles this URI and HTTP method combination.
  3. The method is invoked. Spring resolves its parameters (path variables, query params, request body) using registered HandlerMethodArgumentResolvers.
  4. The return value is processed by a HandlerMethodReturnValueHandler. Because @ResponseBody is present, the value is passed to the negotiated HttpMessageConverter (Jackson for JSON) which writes bytes into the response output stream.
  5. Tomcat flushes the response to the client.
Keep controllers thin. The controller's sole responsibilities are: receive the HTTP request, delegate to a service or use-case class, and map the result to an HTTP response. Business logic, database access, and validation rules belong in the service layer. A controller method longer than about ten lines is a smell that business logic has leaked into the wrong layer.

A Minimal but Complete Example

Below is a self-contained example using Spring Boot 3 with jakarta.* imports. It demonstrates the package structure, a simple DTO record, the service boundary, and the controller wired together.

// ProductDto.java — a Java record is ideal for read-only DTOs package com.example.catalog.product; public record ProductDto(Long id, String name, double price) {}
// ProductService.java — the service interface package com.example.catalog.product; import java.util.List; public interface ProductService { List<ProductDto> findAll(); ProductDto findById(Long id); }
// ProductController.java package com.example.catalog.product; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/api/v1/products") public class ProductController { private final ProductService service; public ProductController(ProductService service) { this.service = service; } @GetMapping public List<ProductDto> list() { return service.findAll(); } @GetMapping("/{id}") public ProductDto get(@PathVariable Long id) { return service.findById(id); } }

With this structure in place, Spring Boot auto-discovers both beans (component scan), wires the service into the controller, and maps GET requests. The JSON serialisation of the record fields happens automatically.

Do not expose JPA entities directly from controllers. Returning an @Entity class from a @RestController method leaks your database schema, can trigger lazy-loading exceptions, and makes it difficult to evolve the API independently of the data model. Always define a DTO layer — records are an excellent fit in Java 16+.

Summary

REST is an architectural style built on uniform interfaces, statelessness, and resource-oriented URIs. HTTP verbs carry semantic intent — GET, POST, PUT, PATCH, DELETE — and idempotency determines retry safety. @RestController is a composed stereotype that marks a class as both a Spring-managed controller and a direct body-writer, eliminating the need for per-method @ResponseBody. Jackson wires in automatically via Spring Boot's auto-configuration. Keep controllers thin: accept the request, delegate, return the response. The next lesson covers request mapping in detail — how to route different HTTP methods and path patterns to specific handler methods.