Expert · Spring Boot basics · topic 53 of 60 · 30 min
Building a REST API
Explanation
A controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) { this.service = service; }
@GetMapping // GET /api/products?city=Pune
public List<ProductDto> list(@RequestParam(required = false) String city) {
return service.list(city);
}
@GetMapping("/{id}") // GET /api/products/42
public ResponseEntity<ProductDto> one(@PathVariable long id) {
return service.find(id)
.map(ResponseEntity::ok) // 200 with JSON body
.orElse(ResponseEntity.notFound().build()); // 404
}
@PostMapping // POST /api/products with a JSON body
public ResponseEntity<ProductDto> create(@Valid @RequestBody NewProduct body) {
ProductDto saved = service.create(body);
return ResponseEntity.created(URI.create("/api/products/" + saved.id())).body(saved); // 201
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable long id) {
service.delete(id);
return ResponseEntity.noContent().build(); // 204
}
}Jackson (included with the web starter) converts the returned objects to JSON and the request body JSON into the parameter object. Records work well as request and response types (DTOs, data transfer objects).
Validation
import jakarta.validation.constraints.*;
public record NewProduct(
@NotBlank String name,
@NotBlank String city,
@Min(1) long pricePaise,
@PositiveOrZero int stock) { }
// With @Valid on the parameter, invalid input is rejected with 400 Bad Request before your code runs.
// Needs the spring-boot-starter-validation dependency.Clean error responses
@RestControllerAdvice
public class ApiErrors {
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> notFound(ProductNotFoundException e) {
return Map.of("error", e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> invalid(MethodArgumentNotValidException e) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().forEach(f -> errors.put(f.getField(), f.getDefaultMessage()));
return errors;
}
}Status codes to use
| Code | Meaning | Typical use |
|---|---|---|
| 200 OK | Success with a body | GET, PUT |
| 201 Created | A new resource was created | POST (with a Location header) |
| 204 No Content | Success, no body | DELETE |
| 400 Bad Request | Invalid input | Validation errors |
| 401 / 403 | Not logged in / not allowed | Security |
| 404 Not Found | No such resource | Unknown id |
| 409 Conflict | Request conflicts with current state | Duplicate SKU, no seats left |
| 500 Internal Server Error | Unexpected failure | Bugs; never on purpose |
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Building a REST API · 1/6
In this lesson you will build a REST API with Spring Boot.
Prefer a video by a teacher? Find free videos on “spring boot rest api tutorial restcontroller responseentity validation” on YouTube ↗
In real life: Adding a product from the command line
A developer at a retail chain checks the new products API before handing it to the tablet-app team. One curl command sends JSON to the POST endpoint; the reply shows 201 Created and the Location of the new product, exactly as the controller specified.
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Steel bottle","city":"Pune","pricePaise":34900,"stock":12}'
# HTTP/1.1 201 Created
# Location: /api/products/1Practice (0/3 done)
Which annotation reads the 42 in GET /api/products/42 mapped as "/{id}"?
The same controller logic in JavaScript, which runs here: write getProduct(params, products) where params.id arrives as TEXT. Return { status: 200, body: product } when found, { status: 404, body: { error: "Product not found" } } otherwise, and { status: 400, body: { error: "id must be a number" } } when the id is not a whole number.
In a Spring Boot project, build a /api/expenses REST API backed by an in-memory list: GET all, GET by id (404 if missing), POST with validation (description @NotBlank, amount @Positive) returning 201 with a Location header, and DELETE returning 204. Add a @RestControllerAdvice that returns validation errors as JSON. Test every endpoint with curl or Postman.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Layers: controller, service and repositorySpring Boot basics · same module
- Spring and dependency injectionSpring Boot basics · same module
- REST APIs and JSONComputer Networking (CCNA level) · Automation, SDN and cloud networking
- Routing, model binding, validation and status codesC# and .NET · Web APIs with ASP.NET Core and EF Core
- REST API design and JSON handlersGo Programming (backend and cloud) · Building a REST API with a database
- How web requests work (and why that matters)Ethical Hacking and Penetration Testing (legal labs) · Web application testing: Burp Suite and the basics
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which status code should a successful POST that creates a resource return?
Answer: 201
201 Created, ideally with a Location header.
What converts Java objects to JSON in a Spring Boot web app?
Answer: Jackson
Jackson is included with the web starter.
What does @Valid do on a @RequestBody parameter?
Answer: Runs the jakarta.validation constraints and rejects invalid input with 400
Validation happens before your method body.
Why return DTOs instead of JPA entities from controllers?
Answer: To avoid leaking internal fields and coupling the API to the database model
DTOs give a stable, safe API shape.
Which test annotation loads only the web layer for controller tests?Related
Answer: @WebMvcTest
@WebMvcTest is a test slice for MVC controllers.
What does "stateless" mean for a REST API?Related
Answer: Each request carries everything needed, such as the token; the server does not remember earlier requests
Stateless means no memory of earlier requests, so every request includes its own authentication and data.
From REST APIs and JSON
Which status is right for a successful DELETE with nothing to return?Related
Which HTTP method should delete a resource?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Spring Boot basics” before its exam: Layers: controller, service and repository, JPA and Spring Data repositories.
- Module 11 exam: Spring Boot basics · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Design patterns, JVM internals and interviews. 60 topics left in the course before the final exam.