Spring Boot Basics: Build Your First Java Web App
Controllers and Routing
In this lesson you will
- Map GET, POST and DELETE requests to controller methods
- Read path variables and request parameters
- Return JSON and set status codes
Introduction
A controller receives web requests and returns responses. Spring MVC lets you describe this with annotations on ordinary Java classes. These examples are written for Spring Boot 4.1.2 and are not compiled or run here, as explained in the first lesson.
A record as data
Java records are ideal for simple data. Spring converts them to JSON automatically (using the Jackson library, which in Spring Boot 4 is Jackson 3).
Java
package com.example.demo.book;
public record Book(long id, String title, int year) {}Returning new Book(1, "Dune", 1965) produces:
JSON
{"id":1,"title":"Dune","year":1965}A controller with several routes
Java
package com.example.demo.book;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
public class BookController {
private final Map<Long, Book> books = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
public BookController() {
long id = nextId.getAndIncrement();
books.put(id, new Book(id, "Dune", 1965));
}
@GetMapping
public List<Book> list(@RequestParam(defaultValue = "100") int limit) {
return books.values().stream().limit(limit).toList();
}
@GetMapping("/{id}")
public ResponseEntity<Book> get(@PathVariable long id) {
Book book = books.get(id);
if (book == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(book);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable long id) {
books.remove(id);
}
}(You also need import org.springframework.web.bind.annotation.ResponseStatus;.)
Reading it:
@RequestMapping("/books")on the class sets a common prefix.@GetMappingwith no path handlesGET /books.@GetMapping("/{id}")handlesGET /books/5.@PathVariable long idtakes the{id}from the URL and converts it to a number. A non-numeric value like/books/abcproduces a 400 Bad Request response.@RequestParam(defaultValue = "100") int limitreads?limit=5from the query string, with a default.ResponseEntitylets you choose the status code and body.notFound()gives 404.@ResponseStatus(HttpStatus.NO_CONTENT)returns 204 for a successful delete.- The in-memory map is for learning. It is lost on restart, and a later lesson uses a database.
ConcurrentHashMapandAtomicLongare used because requests are handled by several threads at once.
Expected behaviour:
| Request | Result |
|---|---|
GET /books | 200, a JSON array with Dune |
GET /books/1 | 200, the Dune object |
GET /books/99 | 404 |
DELETE /books/1 | 204 |
Quick quiz
Which annotation reads the {id} part of the URL /books/{id}?
Annotations for each method
| HTTP method | Annotation |
|---|---|
| GET | @GetMapping |
| POST | @PostMapping |
| PUT | @PutMapping |
| PATCH | @PatchMapping |
| DELETE | @DeleteMapping |
Design habits
- Use nouns and plurals:
/books, not/getBooks. - Return proper status codes: 201 for created, 404 for not found, 400 for bad input.
- Keep controllers thin. They translate HTTP to method calls. The next course puts logic in services.
- Do not expose internal details in error messages.
Try it yourself
Add a GET /books/search?title=... endpoint that returns books whose title contains the text, ignoring case. Test it with curl, a browser or an HTTP client in your IDE.
Summary
- Annotate a class with @RestController and map methods with @GetMapping, @PostMapping and friends.
- @PathVariable reads path parts, @RequestParam reads query parameters, and records become JSON.
- Use ResponseEntity or @ResponseStatus to return the correct status code.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading