REST Controllers Interview Questions & Answers
Spring MVC REST controllers explained — @RestController, @RequestMapping and the @GetMapping/@PostMapping shortcuts, path variables, request params, ResponseEntity, content negotiation, and how the DispatcherServlet routes a request.
@RestController is a convenience annotation that combines @Controller + @ResponseBody.
It marks a class as a web handler whose methods return data written straight to the response
body (usually JSON), not the name of a view to render.
@RestController // = @Controller + @ResponseBody on every method
@RequestMapping("/api/orders")
class OrderController {
@GetMapping("/{id}")
Order byId(@PathVariable Long id) {
return service.find(id); // serialized to JSON by Jackson, no view lookup
}
}
With a plain @Controller, a returned String is treated as a view name for a template
engine. @RestController flips that: the return value is the response payload. It's the
default choice for building APIs.
Rule of thumb: Use @RestController for JSON/XML APIs; reserve @Controller for
server-rendered HTML pages.
@RequestMapping is the general-purpose mapping annotation; @GetMapping, @PostMapping,
@PutMapping, @PatchMapping, and @DeleteMapping are composed shortcuts that pin the
HTTP method for you.
@RequestMapping(value = "/orders", method = RequestMethod.GET) // verbose
List<Order> all() { ... }
@GetMapping("/orders") // identical, idiomatic
List<Order> all2() { ... }
Each shortcut is meta-annotated with @RequestMapping(method = ...). Put a class-level
@RequestMapping("/api") on the controller to define a base path, then method-level
shortcuts append to it. The shortcuts read better and make the verb obvious at a glance.
Rule of thumb: Use a class-level @RequestMapping for the base path and method-level
@GetMapping/@PostMapping shortcuts for each handler.
Use @PathVariable to bind a {placeholder} segment of the URI template to a method
parameter. The path template lives in the mapping annotation.
@GetMapping("/users/{userId}/orders/{orderId}")
Order get(@PathVariable Long userId,
@PathVariable Long orderId) { // names match {placeholders}
return service.find(userId, orderId);
}
If the parameter name differs from the placeholder, name it explicitly:
@PathVariable("userId") Long id. Spring converts the raw string to the declared type
(Long, UUID, an enum…) automatically; a bad value yields a 400 before your code runs.
Rule of thumb: @PathVariable is for identifying a resource in the path; keep the
placeholder name and parameter name the same so you can omit the explicit value.
Use @RequestParam to bind query-string (or form) parameters like ?page=2&size=20.
@GetMapping("/orders")
Page<Order> list(
@RequestParam(defaultValue = "0") int page, // ?page=
@RequestParam(defaultValue = "20") int size, // ?size=
@RequestParam(required = false) String status) { // optional → null if absent
return service.list(page, size, status);
}
Key attributes: required (default true → 400 if missing), defaultValue (implies
required = false), and an explicit name when the parameter and variable differ. Bind a
Map<String,String> to grab everything, or a List<String> for repeated params.
Rule of thumb: @RequestParam is for filtering/paging/options; give optional ones a
defaultValue so callers can omit them.
Use @PathVariable to identify which resource you're acting on, and @RequestParam
to modify or filter the request. The path names the thing; the query string tunes the view
of it.
// /products/42 → the product is part of the resource's identity
@GetMapping("/products/{id}")
Product one(@PathVariable Long id) { ... }
// /products?category=books&sort=price → filtering/sorting options
@GetMapping("/products")
List<Product> search(@RequestParam String category,
@RequestParam(defaultValue = "name") String sort) { ... }
A clean REST URL puts hierarchy and identity in the path and options in the query string. Optional, combinable, omit-able values belong in query params; mandatory identifiers belong in the path.
Rule of thumb: Path = what resource, query = how you want it. Identity in the path, options in the query string.
Annotate a parameter with @RequestBody and Spring uses an HttpMessageConverter
(Jackson for JSON) to deserialize the body into your object.
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED) // 201 instead of default 200
Order create(@Valid @RequestBody CreateOrderRequest body) {
return service.create(body); // body populated from JSON
}
record CreateOrderRequest(String sku, int qty) {}
Only one @RequestBody per method — the body is a single stream you can read once. Pair it
with @Valid to trigger Bean Validation on the incoming object. A body that won't parse
produces an HttpMessageNotReadableException → 400.
Rule of thumb: Use @RequestBody for the JSON payload of POST/PUT/PATCH, validate it with
@Valid, and bind to an immutable DTO/record rather than your entity.
ResponseEntity<T> is a wrapper that lets you control the full HTTP response — status code,
headers, and body — instead of just returning the body and accepting defaults.
@PostMapping("/orders")
ResponseEntity<Order> create(@RequestBody CreateOrderRequest req) {
Order saved = service.create(req);
return ResponseEntity
.created(URI.create("/api/orders/" + saved.id())) // 201 + Location header
.body(saved);
}
@GetMapping("/orders/{id}")
ResponseEntity<Order> find(@PathVariable Long id) {
return service.findOptional(id)
.map(ResponseEntity::ok) // 200 + body
.orElse(ResponseEntity.notFound().build()); // 404, no body
}
Returning a bare object always gives 200 (or whatever @ResponseStatus says). Reach for
ResponseEntity when the status varies at runtime, when you need to set headers like
Location or ETag, or to return an empty body cleanly.
Rule of thumb: Return the plain object for the simple happy path; use ResponseEntity when
status code or headers depend on the outcome.
For a fixed status, annotate the handler with @ResponseStatus. For a status that
varies, return a ResponseEntity.
@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED) // always 201 on success
Order create(@RequestBody CreateOrderRequest req) { return service.create(req); }
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // always 204, no body
void delete(@PathVariable Long id) { service.delete(id); }
Without either, Spring returns 200 for a non-void method and 200 for void too. @ResponseStatus
is cleanest when the code is constant; ResponseEntity.status(...) wins when you decide the
code based on logic (created vs. already-existed, etc.).
Rule of thumb: @ResponseStatus for a constant code (201 on create, 204 on delete);
ResponseEntity when the status is computed.
The DispatcherServlet is Spring MVC's front controller — a single servlet that
receives every request and orchestrates the components that handle it.
request → DispatcherServlet
→ HandlerMapping (which @RequestMapping method matches?)
→ HandlerAdapter (invoke it, resolve @PathVariable/@RequestBody args)
→ your controller method
→ HttpMessageConverter (serialize return value → JSON)
→ response
It consults a HandlerMapping to find the matching handler, a HandlerAdapter to
invoke it (binding arguments via HandlerMethodArgumentResolvers), then runs the return value
through HttpMessageConverters or a ViewResolver. Spring Boot auto-registers it at / so
you never wire it up yourself.
Rule of thumb: Everything in Spring MVC hangs off the DispatcherServlet front controller —
mappings find your method, adapters invoke it, converters render the result.
Through content negotiation: Spring inspects the request's Accept header (and,
depending on config, a path extension or format param) and picks the matching
HttpMessageConverter.
@GetMapping(value = "/orders/{id}",
produces = { MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE })
Order get(@PathVariable Long id) { return service.find(id); }
// Accept: application/xml → XML (if Jackson XML / JAXB is on the classpath)
// Accept: application/json → JSON (default)
JSON works out of the box because jackson-databind is on the classpath; XML needs
jackson-dataformat-xml or JAXB. The produces attribute restricts what a handler will
emit, and consumes restricts what body types it accepts — a mismatch yields 406 Not
Acceptable or 415 Unsupported Media Type.
Rule of thumb: Content negotiation is driven by the Accept header; add the right Jackson
dataformat dependency to support a new media type and constrain handlers with produces/consumes.
consumes narrows a mapping to requests whose Content-Type matches (what the handler
accepts); produces narrows it to requests whose Accept matches (what the handler
emits). They both filter routing and document the contract.
@PostMapping(value = "/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, // only multipart bodies
produces = MediaType.APPLICATION_JSON_VALUE) // always replies JSON
UploadResult upload(@RequestPart MultipartFile file) { ... }
A request with the wrong Content-Type fails the consumes filter → 415; a client whose
Accept can't be satisfied by produces → 406. You can also use them to route two handlers
on the same URL to different content types.
Rule of thumb: consumes = the body types I accept (matched to Content-Type); produces
= the types I return (matched to Accept).
Controllers are singletons by default — one instance serves all concurrent requests — so they must be stateless and thread-safe. The framework keeps per-request data off the instance.
@RestController
class CounterController {
private int hits; // BUG: shared mutable state across all threads
@GetMapping("/hit")
int hit() { return ++hits; } // race condition under concurrency
}
Each request runs on its own thread, but they share that single controller bean. Request-scoped
data arrives as method parameters (@PathVariable, @RequestBody, HttpServletRequest),
not instance fields. Injected dependencies (services, repositories) are themselves stateless
singletons, so the whole stack stays safe.
Rule of thumb: Treat controllers as stateless singletons — keep request data in method parameters and locals, never in mutable instance fields.
Bind it with @RequestHeader, just like @RequestParam but sourced from the request
headers.
@GetMapping("/me")
User me(@RequestHeader("Authorization") String auth,
@RequestHeader(value = "X-Tenant", required = false) String tenant) {
return service.resolve(auth, tenant); // tenant is null if header absent
}
Same attributes as @RequestParam: required and defaultValue. Bind a
Map<String,String> or HttpHeaders to read all headers at once. For cookies there's the
parallel @CookieValue.
Rule of thumb: @RequestHeader for one header (mark optional ones required = false);
HttpHeaders/Map to read them all.
Yes — @RequestMapping (and the shortcuts) accept arrays of paths and methods, so one
handler can cover several routes.
@RequestMapping(
value = { "/health", "/healthz", "/status" }, // any of these paths
method = { RequestMethod.GET, RequestMethod.HEAD })
String health() { return "OK"; }
@GetMapping({ "/orders", "/orders/" }) // with and without trailing slash
List<Order> list() { ... }
This is handy for aliases, supporting HEAD alongside GET, or accepting legacy paths
during a migration. Keep it sparing — too many aliases on one method hurts readability.
Rule of thumb: Pass an array of paths/methods to map several routes to one handler; use it for aliases and migrations, not as a default.
Model resources as nouns, use HTTP verbs for actions, and return appropriate status codes. The URL names the thing; the method says what to do with it.
@RestController
@RequestMapping("/api/v1/orders") // versioned, plural noun
class OrderController {
@GetMapping List<Order> list() { ... } // 200
@GetMapping("/{id}") Order get(@PathVariable Long id) { } // 200 / 404
@PostMapping @ResponseStatus(CREATED) Order create(...) { } // 201
@PutMapping("/{id}") Order replace(...) { } // 200
@DeleteMapping("/{id}") @ResponseStatus(NO_CONTENT) void delete(...) { } // 204
}
Prefer plural collection nouns (/orders), keep verbs out of paths (not /getOrder),
version the API (/v1), use the right method (POST create, PUT/PATCH update, DELETE
remove), and never expose JPA entities directly — map to DTOs.
Rule of thumb: Nouns in the path, verbs as HTTP methods, correct status codes, DTOs at the boundary — that's a clean REST controller.
More Web MVC interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.