Definition
- @RestController -- combines
@Controllerand@ResponseBody, meaning every handler method returns a serialized object (JSON by default) instead of a view name. - @RequestMapping -- maps HTTP requests to handler methods by URL pattern and HTTP method;
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMappingare shortcuts. - @PathVariable -- extracts a value from the URL path (e.g.,
/accounts/{id}) for resource identification. - @RequestParam -- extracts a value from the query string (e.g.,
?page=1&size=20) for filtering, sorting, and pagination. - @RequestBody -- deserializes the JSON request body into a Java object using Jackson's
MappingJackson2HttpMessageConverter. - @Valid -- triggers Bean Validation (
@NotBlank,@Size,@Min) on the deserialized object; failures throwMethodArgumentNotValidExceptionwith HTTP 400.
At National Bank of India, @RestController is a clerk who processes requests and hands Priya the data directly (JSON). @Controller is a clerk who points Priya to a form on the counter (view template). @PathVariable is the account number written on the request slip. @RequestParam is the filter Priya specifies: "show only transactions above 10000." @RequestBody is the filled-out loan application form Priya submits.
Understanding the Concepts
What is the difference between @RestController and @Controller?
@Controller is a stereotype annotation that marks a class as a web controller. Its handler methods return a view name (e.g., "account-details") that the view resolver maps to an HTML template.
@RestController combines @Controller and @ResponseBody. Every handler method returns an object that Spring serializes to JSON (via Jackson) and writes directly to the HTTP response body.
package com.bank.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// @Controller -- returns a VIEW name
@Controller
public class AccountPageController {
@GetMapping("/pages/accounts/{id}")
public String accountPage(@PathVariable String id, Model model) {
model.addAttribute("accountId", id);
return "account-details"; // resolves to templates/account-details.html
}
}
package com.bank.controller;
import com.bank.model.Account;
import com.bank.service.AccountService;
import org.springframework.web.bind.annotation.*;
// @RestController -- returns JSON directly
@RestController
@RequestMapping("/api/accounts")
public class AccountApiController {
private final AccountService accountService;
public AccountApiController(AccountService accountService) {
this.accountService = accountService;
}
@GetMapping("/{id}")
public Account getAccount(@PathVariable String id) {
return accountService.findById(id); // serialized to JSON automatically
}
}
GET /api/accounts/SAV2001
Response:
{
"id": "SAV2001",
"holderName": "Priya Sharma",
"balance": 50000.00,
"type": "SAVINGS"
}
How do @RequestMapping shortcuts work?
@RequestMapping is the base annotation that maps requests by URL pattern and HTTP method. The shortcut annotations specify the method and keep the code cleaner.
| Shortcut | Equivalent |
|---|---|
| @GetMapping("/accounts") | @RequestMapping(value = "/accounts", method = GET) |
| @PostMapping("/accounts") | @RequestMapping(value = "/accounts", method = POST) |
| @PutMapping("/accounts/{id}") | @RequestMapping(value = "/accounts/{id}", method = PUT) |
| @DeleteMapping("/accounts/{id}") | @RequestMapping(value = "/accounts/{id}", method = DELETE) |
| @PatchMapping("/accounts/{id}") | @RequestMapping(value = "/accounts/{id}", method = PATCH) |
@RequestMapping at the class level sets the base path. Method-level annotations append to it:
package com.bank.controller;
import com.bank.model.Account;
import com.bank.dto.CreateAccountRequest;
import com.bank.service.AccountService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/accounts") // base path
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@GetMapping // GET /api/accounts
public List<Account> list() {
return accountService.findAll();
}
@GetMapping("/{id}") // GET /api/accounts/SAV2001
public Account get(@PathVariable String id) {
return accountService.findById(id);
}
@PostMapping // POST /api/accounts
@ResponseStatus(HttpStatus.CREATED)
public Account create(@Valid @RequestBody CreateAccountRequest req) {
return accountService.create(req);
}
@PutMapping("/{id}") // PUT /api/accounts/SAV2001
public Account update(@PathVariable String id,
@Valid @RequestBody CreateAccountRequest req) {
return accountService.update(id, req);
}
@DeleteMapping("/{id}") // DELETE /api/accounts/SAV2001
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable String id) {
accountService.delete(id);
}
}
How is @PathVariable different from @RequestParam?
@PathVariable extracts a value from the URL path. Use it for resource identification (the thing you want).
@RequestParam extracts a value from the query string. Use it for filtering, sorting, pagination, and optional parameters.
| Aspect | @PathVariable | @RequestParam |
|---|---|---|
| Source | URL path: /accounts/{id} | Query string: ?status=active |
| Purpose | Resource identification | Filtering, pagination, options |
| Required | Yes by default | Yes by default (required=false to opt out) |
| Default value | Not supported | defaultValue = "0" |
| Multiple | /accounts/{id}/transactions/{txId} | ?page=0&size=20&sort=date |
package com.bank.controller;
import com.bank.model.Transaction;
import com.bank.service.TransactionService;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
@RestController
@RequestMapping("/api/accounts")
public class TransactionController {
private final TransactionService txService;
public TransactionController(TransactionService txService) {
this.txService = txService;
}
// @PathVariable for resource identification
// @RequestParam for filtering and pagination
@GetMapping("/{accountId}/transactions")
public List<Transaction> getTransactions(
@PathVariable String accountId,
@RequestParam(required = false) String type,
@RequestParam(defaultValue = "0") BigDecimal minAmount,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return txService.find(accountId, type, minAmount, page, size);
}
}
GET /api/accounts/SAV2001/transactions?type=DEBIT&minAmount=5000&page=0&size=10
accountId = "SAV2001" (from URL path)
type = "DEBIT" (from query string)
minAmount = 5000 (from query string)
page = 0 (from query string)
size = 10 (from query string)
How does @RequestBody work internally?
When a request arrives with Content-Type: application/json, Spring's DispatcherServlet delegates to HandlerAdapter, which uses HandlerMethodArgumentResolver to resolve method parameters. For @RequestBody, RequestResponseBodyMethodProcessor calls Jackson's MappingJackson2HttpMessageConverter to read the input stream and deserialize the JSON bytes into the target Java class.
If the JSON is malformed, Jackson throws HttpMessageNotReadableException (HTTP 400). If @Valid is present, Bean Validation runs after deserialization and throws MethodArgumentNotValidException (HTTP 400) for constraint violations.
package com.bank.dto;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
public record CreateAccountRequest(
@NotBlank(message = "Holder name is required")
String holderName,
@NotNull(message = "Initial deposit is required")
@DecimalMin(value = "1000", message = "Minimum deposit is 1000")
BigDecimal initialDeposit,
@NotBlank(message = "Account type is required")
@Pattern(regexp = "SAVINGS|CURRENT", message = "Type must be SAVINGS or CURRENT")
String accountType
) {}
package com.bank.controller;
import com.bank.dto.CreateAccountRequest;
import com.bank.model.Account;
import com.bank.service.AccountService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Account create(@Valid @RequestBody CreateAccountRequest req) {
return accountService.create(req);
}
}
POST /api/accounts
Content-Type: application/json
{
"holderName": "",
"initialDeposit": 500,
"accountType": "INVALID"
}
Response: 400 Bad Request
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed",
"errors": [
{ "field": "holderName", "message": "Holder name is required" },
{ "field": "initialDeposit", "message": "Minimum deposit is 1000" },
{ "field": "accountType", "message": "Type must be SAVINGS or CURRENT" }
]
}
How do you handle validation errors consistently?
A @RestControllerAdvice (also called @ControllerAdvice) handles exceptions globally across all controllers. For validation errors, catch MethodArgumentNotValidException and return structured error responses.
package com.bank.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Validation Failed");
List<Map<String, String>> errors = ex.getFieldErrors().stream()
.map(e -> Map.of(
"field", e.getField(),
"message", e.getDefaultMessage()
))
.toList();
problem.setProperty("errors", errors);
return problem;
}
@ExceptionHandler(AccountNotFoundException.class)
public ProblemDetail handleNotFound(AccountNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setTitle("Account Not Found");
problem.setDetail(ex.getMessage());
return problem;
}
}
This uses RFC 9457 ProblemDetail format (Spring Boot 3+), which provides a standard structure for error responses.
How do you configure ObjectMapper consistently?
Never create new ObjectMapper() in service code. Spring Boot auto-configures one shared ObjectMapper. Customize it via Jackson2ObjectMapperBuilderCustomizer:
package com.bank.config;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
.failOnUnknownProperties(false);
}
}
This shared ObjectMapper is used by @RequestBody deserialization, @ResponseBody serialization, RestTemplate, WebClient, and ObjectMapper injection everywhere in the application. All JSON handling is consistent.
WRONG -- creating a separate ObjectMapper:
package com.bank.service;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ReportService {
// WRONG: different configuration from the shared mapper
private final ObjectMapper mapper = new ObjectMapper();
public String toJson(Object report) throws Exception {
return mapper.writeValueAsString(report); // dates as timestamps!
}
}
CORRECT -- inject the shared ObjectMapper:
package com.bank.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
@Service
public class ReportService {
private final ObjectMapper mapper; // shared, properly configured
public ReportService(ObjectMapper mapper) {
this.mapper = mapper;
}
public String toJson(Object report) throws Exception {
return mapper.writeValueAsString(report); // consistent formatting
}
}
What is the DispatcherServlet request flow?
Every HTTP request in Spring MVC flows through DispatcherServlet, which is the front controller:
- DispatcherServlet receives the request.
- HandlerMapping matches the URL and HTTP method to a controller method.
- HandlerAdapter resolves method arguments (
@PathVariable,@RequestParam,@RequestBody). - Controller method executes business logic and returns a result.
- HttpMessageConverter (Jackson) serializes the return value to JSON.
- Response is written back to the client.
package com.bank.controller;
import com.bank.model.Account;
import com.bank.service.AccountService;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
// Full CRUD demonstrating all annotation types
@GetMapping // list all
public java.util.List<Account> list() {
return accountService.findAll();
}
@GetMapping("/{id}") // get by ID (PathVariable)
public Account get(@PathVariable String id) {
return accountService.findById(id);
}
@GetMapping("/search") // search with query params
public java.util.List<Account> search(
@RequestParam String holderName,
@RequestParam(defaultValue = "0") BigDecimal minBalance) {
return accountService.search(holderName, minBalance);
}
@PostMapping // create (RequestBody + Valid)
@org.springframework.http.ResponseEntity
public Account create(
@jakarta.validation.Valid @RequestBody com.bank.dto.CreateAccountRequest req) {
return accountService.create(req);
}
}
What are common mistakes with request mapping?
WRONG -- using @Controller when you want JSON:
package com.bank.controller;
import com.bank.model.Account;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller // returns VIEW names, not JSON
public class AccountController {
@GetMapping("/api/accounts/{id}")
public Account get(@PathVariable String id) {
return accountService.findById(id);
// ERROR: Spring tries to find a view named "Account"
// jakarta.servlet.ServletException: Circular view path
}
}
CORRECT -- use @RestController for JSON APIs:
@RestController // @Controller + @ResponseBody
public class AccountController {
@GetMapping("/api/accounts/{id}")
public Account get(@PathVariable String id) {
return accountService.findById(id); // serialized to JSON
}
}
WRONG -- forgetting @Valid on @RequestBody:
@PostMapping
public Account create(@RequestBody CreateAccountRequest req) {
// Validation annotations on CreateAccountRequest are IGNORED
// Invalid data reaches the service layer
return accountService.create(req);
}
CORRECT -- add @Valid to trigger validation:
@PostMapping
public Account create(@Valid @RequestBody CreateAccountRequest req) {
// Validation runs: @NotBlank, @DecimalMin, @Pattern enforced
// Invalid data returns 400 with error details
return accountService.create(req);
}
What should a senior engineer know?
- Content negotiation. Spring MVC supports multiple response formats. The
producesattribute on@GetMapping(produces = "application/xml")controls the format. Jackson's XML module or JAXB handles XML serialization. - Custom HttpMessageConverters. For non-JSON formats (CSV, Protocol Buffers), register a custom
HttpMessageConverter. Spring auto-detects beans of this type. - ResponseEntity for full control.
ResponseEntitylets you set status code, headers, and body. Use it for conditional responses:ResponseEntity.ok(account),ResponseEntity.notFound().build(),ResponseEntity.created(uri).body(account). - Async request handling. Return
Callable,DeferredResult, orCompletableFutureto release the Tomcat thread while waiting for a slow operation. The response is written when the result completes. - API versioning. Common strategies: URL path (
/api/v1/accounts), header (Accept: application/vnd.bank.v1+json), or query parameter (?version=1). URL path is the most common and discoverable approach.
What are common interview follow-up questions?
- What is the difference between
@Controllerand@RestController? (@RestController adds @ResponseBody to every method.) - What happens if the JSON body is malformed? (HttpMessageNotReadableException, HTTP 400.)
- Can you combine
@PathVariableand@RequestBodyin one method? (Yes: PathVariable from URL, RequestBody from JSON body.) - How do you return HTTP 201 Created? (
@ResponseStatus(HttpStatus.CREATED)orResponseEntity.created(uri).body(obj).) - How does Spring Boot configure Jackson? (Auto-configures ObjectMapper via
spring.jackson.*properties andJackson2ObjectMapperBuilderCustomizerbeans.)
Visual Overview
DispatcherServlet Request Flow:
Client ──→ DispatcherServlet
│
├── HandlerMapping (match URL + method)
│ └── /api/accounts/{id} + GET
│ → AccountController.get()
│
├── HandlerAdapter (resolve arguments)
│ ├── @PathVariable id ← URL path
│ ├── @RequestParam ← query string
│ └── @RequestBody ← JSON body (Jackson)
│
├── Controller Method (business logic)
│ └── return Account object
│
├── HttpMessageConverter (serialize)
│ └── Jackson → JSON bytes
│
└── HTTP Response → Client
200 OK
Content-Type: application/json
{ "id": "SAV2001", ... }
Annotation Mapping:
@PathVariable ← /accounts/{id} → resource ID
@RequestParam ← ?page=0&size=20 → filters
@RequestBody ← { JSON body } → request object
Key Takeaway
@RestController combines @Controller and @ResponseBody, meaning every handler method returns serialized JSON instead of a view name. The shortcut annotations @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping specify both the URL path and the HTTP method, keeping controller code clean and readable. @PathVariable extracts values from the URL path for resource identification, like the account ID in /accounts/SAV2001. @RequestParam extracts values from the query string for filtering, pagination, and optional parameters. @RequestBody deserializes the JSON body into a Java object via Jackson's HttpMessageConverter, and when combined with @Valid, Bean Validation runs constraint annotations like @NotBlank and @DecimalMin automatically.
In Rajesh's banking API at National Bank of India, the AccountController demonstrates all annotation types in one CRUD service: POST with @RequestBody and @Valid creates an account from JSON, GET with @PathVariable retrieves by ID, GET with @RequestParam searches by holder name and minimum balance, PUT combines @PathVariable and @RequestBody to update an account, and DELETE with @ResponseStatus NO_CONTENT removes an account. The GlobalExceptionHandler catches validation failures and returns structured 400 responses with per-field error messages using Spring Boot 3's RFC 9457 ProblemDetail format, giving Priya clear feedback when her loan application form has invalid fields.
The critical configuration rule is to never create new ObjectMapper() manually. Spring Boot auto-configures one shared ObjectMapper that is used by @RequestBody deserialization, @ResponseBody serialization, RestTemplate, WebClient, and all injected ObjectMapper references. Customize it via Jackson2ObjectMapperBuilderCustomizer so all JSON handling in the application is consistent.
Remember one sentence: @RestController returns JSON, @PathVariable identifies the resource, @RequestParam filters it, @RequestBody deserializes the body, and @Valid validates it.
---