Skip to content

REST Controllers Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

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.

Read the in-depth guideBuilding REST Controllers in Spring Boot, Explained(opens in new tab)
15 of 15

@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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel