Course topics

By WebNest Studio Engineering Team

Spring Boot Tutorial

Building REST APIs with Spring Boot

Spring MVC, combined with Spring Boot's auto-configuration, is the standard way to expose RESTful HTTP endpoints in a Java backend. A REST controller maps incoming HTTP requests to Java methods, and Spring handles serialization, deserialization, routing, and status codes based on annotations.

@RestController and Mapping Annotations

@RestController is a specialization of @Controller that adds @ResponseBody to every method, meaning return values are written directly to the HTTP response body (typically as JSON, via Jackson) instead of being resolved as a view name. Method-level mapping annotations tie a method to an HTTP verb and path.

  • @GetMapping("/api/products") — handles HTTP GET, typically for retrieving data.
  • @PostMapping("/api/products") — handles HTTP POST, typically for creating a resource.
  • @PutMapping("/api/products/{id}") — handles HTTP PUT, typically for a full update.
  • @DeleteMapping("/api/products/{id}") — handles HTTP DELETE, for removing a resource.

Reading Request Data

@PathVariable extracts a value from the URL path (e.g. the {id} in /products/{id}), @RequestParam reads a query-string parameter (e.g. ?page=2), and @RequestBody deserializes the incoming JSON request body directly into a Java object using Jackson.

Controlling the Response with ResponseEntity

Returning a plain object always responds with HTTP 200 and that object serialized as JSON. When you need to control the exact status code or headers — 201 Created after a POST, 404 Not Found when a resource doesn't exist — wrap the response in ResponseEntity<T>, which gives full control over status, headers, and body together.

Designing the URLs

Good REST URLs name things, not actions. Use plural nouns for collections (/api/tasks), an ID for one item (/api/tasks/42), and nesting only for real ownership (/api/projects/7/tasks). The HTTP method says what to do, so POST /api/tasks replaces /api/createTask and DELETE /api/tasks/42 replaces /api/deleteTask?id=42. Filters, sorting and paging go in the query string: /api/tasks?done=false&sort=dueDate.

Status Codes for Each Operation

  • GET one item: 200 with the item, or 404 if it does not exist. GET a collection: 200, with an empty list rather than 404 when nothing matches.
  • POST (create): 201 Created, the new item in the body, and a Location header pointing at its URL. ResponseEntity.created(uri) sets both status and header.
  • PUT (replace the whole item): 200 with the updated item, or 404.
  • PATCH (change some fields): 200. Only offer it when clients really need partial updates, and remember that a missing field and a field set to null mean different things.
  • DELETE: 204 No Content with an empty body, or 404.
  • A method you did not map, such as PATCH on a resource that only supports PUT, gets 405 Method Not Allowed from Spring automatically.

Safe and Idempotent Methods

GET must never change data, because browsers, crawlers and caches feel free to repeat it. PUT and DELETE are idempotent: sending the same request twice leaves the server in the same state as sending it once, so a client can safely retry after a timeout. POST is not idempotent, so a retried POST can create a duplicate order. If clients may retry creates, accept an idempotency key header and ignore repeats of a key you have already processed.

Examples

A REST controller with full CRUD-style mappings using ResponseEntity

Java
@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService service;

    public ProductController(ProductService service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getById(@PathVariable Long id) {
        return service.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Product> create(@RequestBody Product product) {
        Product saved = service.save(product);
        return ResponseEntity.status(HttpStatus.CREATED).body(saved);
    }

    @GetMapping
    public List<Product> list(@RequestParam(defaultValue = "0") int page) {
        return service.findPage(page);
    }
}
Output
GET /api/products/5   -> 200 OK   {"id":5,"name":"Wireless Mouse","price":19.99}
POST /api/products    -> 201 Created {"id":6,"name":"USB Hub","price":24.5}
GET /api/products?page=0 -> 200 OK [{"id":1,...}, {"id":2,...}]

A complete CRUD API with records, a Location header and 204 on delete (tested on Spring Boot 4.1.1)

Java
@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    public record TaskRequest(String title, boolean done) {}
    public record Task(long id, String title, boolean done) {}

    // In-memory storage keeps the example self-contained; a real API calls a service.
    private final Map<Long, Task> tasks = new ConcurrentHashMap<>();
    private final AtomicLong nextId = new AtomicLong(1);

    @GetMapping
    public Collection<Task> list() {
        return tasks.values();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Task> get(@PathVariable long id) {
        Task task = tasks.get(id);
        return task == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(task);
    }

    @PostMapping
    public ResponseEntity<Task> create(@RequestBody TaskRequest request) {
        Task task = new Task(nextId.getAndIncrement(), request.title(), request.done());
        tasks.put(task.id(), task);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}").buildAndExpand(task.id()).toUri();
        return ResponseEntity.created(location).body(task);   // 201 + Location header
    }

    @PutMapping("/{id}")
    public ResponseEntity<Task> replace(@PathVariable long id, @RequestBody TaskRequest request) {
        if (!tasks.containsKey(id)) {
            return ResponseEntity.notFound().build();
        }
        Task task = new Task(id, request.title(), request.done());
        tasks.put(id, task);
        return ResponseEntity.ok(task);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable long id) {
        return tasks.remove(id) == null
                ? ResponseEntity.notFound().build()
                : ResponseEntity.noContent().build();                   // 204, no body
    }
}
Output
$ curl -i -X POST localhost:8080/api/tasks -H "Content-Type: application/json" -d '{"title":"Write the invoice","done":false}'
HTTP/1.1 201
Location: http://localhost:8080/api/tasks/1
{"id":1,"title":"Write the invoice","done":false}

GET    /api/tasks/1  -> 200 {"id":1,"title":"Write the invoice","done":false}
PUT    /api/tasks/1  -> 200 {"id":1,"title":"Write the invoice","done":true}
GET    /api/tasks    -> 200 [{"id":1,"title":"Write the invoice","done":true}]
DELETE /api/tasks/1  -> 204 (empty body)
GET    /api/tasks/1  -> 404 (empty body: ResponseEntity.notFound() sends no JSON)
PATCH  /api/tasks/1  -> 405 {"status":405,"error":"Method Not Allowed","path":"/api/tasks/1",...}

Common Mistakes

  • Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code.
  • Returning 404 for a collection that happens to be empty. GET /api/tasks?done=true with no matches should be 200 with [].
  • Putting verbs in URLs (/api/getTasks, /api/deleteTask) instead of letting the HTTP method carry the action.
  • Using @Controller instead of @RestController and forgetting @ResponseBody, causing Spring to try (and fail) to resolve the return value as a view template name.
  • Returning the raw entity object for every case instead of ResponseEntity, which makes it impossible to return a proper 404 or 201 with the right semantics.
  • Mismatching @PathVariable name and the {placeholder} in the mapping path when they are not identical identifiers, without using the explicit @PathVariable("id") form.
  • Putting business logic directly inside the controller method instead of delegating to a service layer, which makes the class hard to test and reuse.

Exercise

Using the lesson's A REST controller with full CRUD-style mappings using ResponseEntity example, create a small implementation or design sketch that demonstrates @RestController = @Controller + @ResponseBody, meaning return values are serialized straight to the response body.. Add a test case or scenario that would catch Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code., and describe the expected result.

Show hints
  • Start with @RestController and Mapping Annotations and compare your approach with A REST controller with full CRUD-style mappings using ResponseEntity. Identify the decision or behavior the lesson recommends.
  • Walk through your scenario step by step. Confirm that it follows @RestController = @Controller + @ResponseBody, meaning return values are serialized straight to the response body. and that it would catch the failure described by Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code..

Quick quiz

1. Which choice is a mistake to avoid in Building REST APIs with Spring Boot?

  • A. Returning 404 for a collection that happens to be empty. GET /api/tasks?done=true with no matches should be 200 with [].
  • B. @RestController = @Controller + @ResponseBody, meaning return values are serialized straight to the response body.
  • C. Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code.
Show answer

C. Avoid this approach: Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code. The lesson recommends: @RestController = @Controller + @ResponseBody, meaning return values are serialized straight to the response body.

Interview questions

A teammate is about to make this mistake: Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code. How would you diagnose and correct it using the lesson's guidance?

Show answer

@RestController = @Controller + @ResponseBody, meaning return values are serialized straight to the response body. Apply this in @RestController and Mapping Annotations; the example A REST controller with full CRUD-style mappings using ResponseEntity shows one way to do it. Avoid Returning 200 with an error message in the body, such as {"success":false}, instead of a real 4xx or 5xx status. Clients, monitoring and caches all rely on the status code. and explain how your implementation or design prevents that failure.

Key Points to Remember

  • @RestController = @Controller + @ResponseBody, meaning return values are serialized straight to the response body.
  • @GetMapping/@PostMapping/@PutMapping/@DeleteMapping map HTTP verbs to controller methods.
  • @PathVariable reads URL segments, @RequestParam reads query parameters, @RequestBody deserializes the JSON body.
  • ResponseEntity<T> gives full control over status code, headers, and body for the response.
  • Use plural nouns in URLs and let the HTTP method express the action.
  • POST returns 201 with a Location header, DELETE returns 204, and missing items return 404.
  • GET is safe, PUT and DELETE are idempotent, and POST needs an idempotency key if clients retry it.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering