Foundation · Build

Your First REST Controller

Route HTTP requests to Java methods with @RestController and the mapping annotations, then grow one endpoint into a small resource API.

Primary source: Spring MVC: Annotated Controllers

15 min

Lesson 1 of 11

1d streak

The Architectural Question

Until you have written one route and called it, every MVC concept floats free of the concrete request it will eventually handle.

What this guide proves

  • @RestController routes
  • @GetMapping and @PostMapping
  • Resource URLs

HTTP dispatching: How Spring MVC maps URLs to Java methods

A REST controller declares the resource contract; it does not own the HTTP dispatch pipeline. Spring MVC routes requests through DispatcherServlet, which is explained step by step in the dedicated DispatcherServlet lesson. Keep this lesson focused on choosing resource names, HTTP methods, and a controller boundary.

Building an idiomatic REST controller

An idiomatic REST controller uses plural nouns for resource paths, enforces immutability via Java records, and delegates business state management to a domain service.

TaskController.java
@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {

    private final TaskService taskService;

    // Single constructor: auto-wired by Spring without @Autowired
    public TaskController(TaskService taskService) {
        this.taskService = Objects.requireNonNull(taskService);
    }

    @GetMapping
    public List<TaskResponse> listTasks() {
        return taskService.findAll();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public TaskResponse createTask(@RequestBody @Valid CreateTaskRequest request) {
        return taskService.create(request);
    }
}

Resource URI Design

URIs identify resources, not operations. Use POST /api/v1/tasks (noun) rather than POST /api/v1/createTask (verb). The HTTP method supplies the verb.

Break it deliberately: The missing @RestController trap

Annotating methods with @GetMapping or @PostMapping on a standard class without @RestController or @Controller causes Spring's RequestMappingHandlerMapping to completely ignore the class during startup.

BrokenController.java
// Missing @RestController or @Controller!
public class TaskController {
    @GetMapping("/api/v1/tasks")
    public List<String> getTasks() {
        return List.of("Task 1", "Task 2");
    }
}
Terminal / Diagnostic Output
$ curl -i http://localhost:8080/api/v1/tasks
HTTP/1.1 404 Not Found
Content-Type: application/json
Transfer-Encoding: chunked

{
  "timestamp": "2026-09-03T10:15:22.124Z",
  "status": 404,
  "error": "Not Found",
  "path": "/api/v1/tasks"
}

Root Cause

RequestMappingHandlerMapping only scans classes annotated with @Controller or @RequestMapping at the class level. Without the stereotype, the endpoints are never registered into the handler mapping table.

Check your understanding

Active Recall Checkpoint

What central Spring MVC servlet acts as the Front Controller for all incoming HTTP requests?

Hint: Think about the core servlet in spring-webmvc.

Show answer & explanation

DispatcherServlet acts as the Front Controller, dispatching requests to handler mappings, invoking handler adapters, and coordinating message converters.

Active Recall Checkpoint

What happens when a class declares @GetMapping methods but lacks @RestController or @Controller at class level?

Hint: Does RequestMappingHandlerMapping scan it?

Show answer & explanation

RequestMappingHandlerMapping ignores the class during startup, resulting in HTTP 404 Not Found for all routes declared on that class.

Working Implementation

Code Walkthrough

Expose GET /api/tasks returning an empty list, then POST /api/tasks that echoes the submitted title back with an id.

FirstRestControllerLab.java
package com.example.web;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RequestMapping("/api/v1/products")
public class FirstRestControllerLab {

    public record Product(String id, String name, double price) {}
    private final Map<String, Product> store = new ConcurrentHashMap<>();

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable String id) {
        Product product = store.get(id);
        if (product == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(product);
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product input) {
        store.put(input.id(), input);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}").buildAndExpand(input.id()).toUri();
        return ResponseEntity.created(location).body(input);
    }
}
@RestController combines @Controller and @ResponseBody, automatically serializing return types to JSON.
Returning ResponseEntity<T> allows explicit control over HTTP status codes, headers, and Location URIs.
@PathVariable extracts path segments while @RequestBody deserializes JSON payloads via Jackson.

Synthesis & Production Verification

Production Acceptance Criteria

  • Serve JSON from GET and accept JSON on POST
  • Name resources with plural nouns and avoid verbs in paths
  • Call both endpoints from curl and read the exchanged headers

Architectural & Technical Review

Core Interview Questions

Q01What responsibilities should a first REST controller demonstrate?Show answer

It should make route selection, request-body binding, status and header semantics, and response serialization visible without mixing in persistence or business complexity.

Q02How do request mapping and message conversion differ?Show answer

Handler mapping selects a method from path, HTTP method, and mapping conditions. Message converters deserialize and serialize representations according to Content-Type, Accept, and Java types.

Q03What does a successful POST contract need beyond a JSON body?Show answer

It should communicate the outcome with 201 Created and identify the new resource with a Location header, then expose a representation whose fields are intentionally public.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Encoding actions in resource paths — Routes such as /createTask duplicate semantics already carried by POST and make a resource API harder to evolve consistently.
  • Pitfall: Hand-building JSON strings — Manual JSON bypasses configured converters, escaping, content negotiation, and reusable contract types.

Primary Documentation & Specifications

Lesson Primary References

Supplementary Module Resources