Skip to content

Spring Boot · Web MVC

Building REST Controllers in Spring Boot, Explained

5 min read Updated 2026-06-26 Share:

Practice REST Controllers interview questions

The annotation that builds your API

Almost every Spring Boot service is, at its surface, a stack of REST controllers. Get them right and the rest of the app has a clean, predictable boundary; get them wrong and you leak entities, return the wrong status codes, and confuse every client. This article walks through how a request becomes a method call and how to shape that method well.

@RestController is @Controller + @ResponseBody

@RestController marks a class whose methods return data written straight to the response body rather than the name of a view to render.

@RestController
@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 a view name for a template engine. @RestController flips that: the return value is the payload. It's the default for APIs.

The mapping shortcuts

@RequestMapping is the general annotation; @GetMapping, @PostMapping, @PutMapping, @PatchMapping, and @DeleteMapping are composed shortcuts that pin the HTTP method.

@RequestMapping(value = "/orders", method = RequestMethod.GET)  // verbose
@GetMapping("/orders")                                          // identical, idiomatic

Put a class-level @RequestMapping("/api/orders") on the controller for the base path, then let each method-level shortcut append to it. The verb is obvious at a glance.

Pulling data out of the request

Three annotations cover almost everything:

@GetMapping("/users/{userId}/orders")
List<Order> list(
    @PathVariable Long userId,                          // {placeholder} in the path
    @RequestParam(defaultValue = "0") int page,         // ?page= query string
    @RequestHeader(value = "X-Tenant", required = false) String tenant) {  // a header
    return service.list(userId, page, tenant);
}

@PostMapping
Order create(@Valid @RequestBody CreateOrderRequest body) {  // JSON body → object
    return service.create(body);
}

The split between @PathVariable and @RequestParam matters: the path identifies the resource, the query string tunes how you want it. /products/42 names a product; /products?category=books filters a collection. Optional, combinable values belong in the query string; mandatory identifiers belong in the path.

Only one @RequestBody per method — the body is a stream you read once — and pair it with @Valid so Bean Validation runs before your logic does.

ResponseEntity when you need full control

Returning a plain object always yields 200 (or whatever @ResponseStatus declares). When the status or headers depend on the outcome, return a ResponseEntity.

@PostMapping
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("/{id}")
ResponseEntity<Order> find(@PathVariable Long id) {
    return service.findOptional(id)
        .map(ResponseEntity::ok)                    // 200 + body
        .orElse(ResponseEntity.notFound().build()); // 404, no body
}

For a fixed status, @ResponseStatus(HttpStatus.CREATED) on the method is cleaner. Reach for ResponseEntity only when the code is computed.

How a request actually gets routed

Every request enters through the DispatcherServlet, Spring MVC's front controller:

request → DispatcherServlet
        → HandlerMapping     (which @RequestMapping method matches?)
        → HandlerAdapter     (invoke it, resolve @PathVariable/@RequestBody args)
        → your controller method
        → HttpMessageConverter (serialize the return value → JSON)
        → response

A HandlerMapping finds the matching method, a HandlerAdapter invokes it (binding arguments via HandlerMethodArgumentResolvers), and an HttpMessageConverter renders the return value. Spring Boot auto-registers the DispatcherServlet at /, so you never wire it up.

Content negotiation

Which format you get back is driven by the request's Accept header and the converters on the classpath. JSON works out of the box because jackson-databind is present; XML needs jackson-dataformat-xml. The produces attribute restricts what a handler emits and consumes restricts what it accepts — a mismatch returns 406 or 415.

Controllers are stateless singletons

A controller is a singleton bean shared by every concurrent request, so it must be stateless. Per-request data arrives as method parameters, never instance fields:

@RestController
class CounterController {
    private int hits;                  // BUG: shared mutable state — race condition
    @GetMapping("/hit") int hit() { return ++hits; }
}

Keep request state in parameters and locals, and your injected services (also stateless singletons) keep the whole stack thread-safe.

Conventions for a clean API

@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
    @DeleteMapping("/{id}") @ResponseStatus(NO_CONTENT) void delete(...) { } // 204
}

Plural nouns in the path, verbs out of the URL (/orders, not /getOrder), a version prefix, the right HTTP method per action, and DTOs at the boundary instead of raw JPA entities.

Recap

@RestController writes return values straight to the body; the mapping shortcuts pin the verb; @PathVariable/@RequestParam/@RequestBody/@RequestHeader bind the request; ResponseEntity and @ResponseStatus shape the response; the DispatcherServlet ties it together; and content negotiation picks the format. Keep controllers stateless, model resources as nouns, use HTTP verbs for actions, and map to DTOs — that's a REST controller an interviewer (and a client) will respect.

More ways to practice

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

Join our WhatsApp Channel