Building RESTful APIs with Spring Boot
Explore the creation of RESTful services using Spring Boot.
Content
Creating REST Controllers
Versions:
Watch & Learn
AI-discovered learning video
Creating REST Controllers in Spring Boot
You set up your Spring Boot project. You nodded solemnly at REST architecture like it was a TED Talk. Now its time to make the app actually talk back. Welcome to creating REST controllers the front door of your microservice, the bouncer at Club API, the person who decides whether your HTTP request gets in or gets ejected with a 404.
If REST is the city plan, a REST controller is the street-level shop where the business actually happens.
Well build on what you already know: resources, HTTP methods, and statelessness. Lets wire the principles to real endpoints without summoning any accidental 500s.
What Is a REST Controller?
A REST controller in Spring Boot is a class that:
- Handles incoming HTTP requests (GET, POST, PUT, PATCH, DELETE)
- Maps them to Java methods using annotations
- Returns resource representations (usually JSON)
- Sets appropriate HTTP status codes and headers
In Spring, youll meet two very similar-sounding annotations. Only one will text you back.
| Annotation | What it does | Typical use |
|---|---|---|
@Controller |
MVC controller for server-side views (e.g., Thymeleaf) | Traditional web apps |
@RestController |
Combines @Controller + @ResponseBody, so method return values are written directly to the HTTP response as JSON |
RESTful APIs |
TL;DR: For REST APIs, use
@RestController. Unless youre rendering HTML like its 2011, in which case, live your truth.
How Does a REST Controller Work?
- You annotate methods with
@GetMapping,@PostMapping, etc. - Springs HTTP message converters (hello, Jackson!) serialize/deserialize JSON automatically.
- You return a body (or not), and Spring takes care of status codes and content type. Or you take control with
ResponseEntity.
Remember from REST principles:
- Resources are nouns (e.g.,
/coffees,/users). - HTTP verbs do the action.
- Idempotency matters: GET/PUT/DELETE are idempotent; POST is not.
Examples of Creating REST Controllers (with Coffee, obviously)
Lets build a tiny REST controller for a Coffee resource. Well keep layers clean and vibes cleaner: a CoffeeController talks to a CoffeeService, returns DTOs, and plays nice with status codes.
Request and Response DTOs
package com.example.coffee.api;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import java.util.UUID;
public record CoffeeRequest(
@NotBlank String name,
@Positive Integer sizeOunces
) {}
public record CoffeeResponse(
UUID id,
String name,
Integer sizeOunces
) {}
The Service Contract (because controllers shouldnt brew coffee directly)
package com.example.coffee.domain;
import com.example.coffee.api.CoffeeRequest;
import com.example.coffee.api.CoffeeResponse;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public interface CoffeeService {
List<CoffeeResponse> findAll(int page, int size);
CoffeeResponse findById(UUID id);
CoffeeResponse create(CoffeeRequest request);
CoffeeResponse replace(UUID id, CoffeeRequest request);
CoffeeResponse updatePartial(UUID id, Map<String, Object> fields);
void delete(UUID id);
}
The REST Controller
package com.example.coffee.api;
import com.example.coffee.domain.CoffeeService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/coffees")
public class CoffeeController {
private final CoffeeService service;
public CoffeeController(CoffeeService service) {
this.service = service;
}
// GET /coffees?page=0&size=20
@GetMapping
public List<CoffeeResponse> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return service.findAll(page, size);
}
// GET /coffees/{id}
@GetMapping("/{id}")
public CoffeeResponse get(@PathVariable UUID id) {
return service.findById(id);
}
// POST /coffees
@PostMapping
public ResponseEntity<CoffeeResponse> create(
@Valid @RequestBody CoffeeRequest request,
UriComponentsBuilder uriBuilder) {
CoffeeResponse created = service.create(request);
URI location = uriBuilder.path("/api/v1/coffees/{id}")
.buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
}
// PUT /coffees/{id}
@PutMapping("/{id}")
public CoffeeResponse replace(
@PathVariable UUID id,
@Valid @RequestBody CoffeeRequest request) {
return service.replace(id, request);
}
// PATCH /coffees/{id}
@PatchMapping("/{id}")
public CoffeeResponse updatePartial(
@PathVariable UUID id,
@RequestBody Map<String, Object> fields) {
return service.updatePartial(id, fields);
}
// DELETE /coffees/{id}
@DeleteMapping("/{id}")
@ResponseStatus(org.springframework.http.HttpStatus.NO_CONTENT)
public void delete(@PathVariable UUID id) {
service.delete(id);
}
}
Key moments:
@RestControllerensures JSON out-of-the-box.@RequestMapping("/api/v1/coffees")scopes all endpoints cleanly.@Valid @RequestBodyactivates validation for input.ResponseEntity.created(location)returns 201 and aLocationheader like a true REST citizen.
201 + Location is the API equivalent of cHeres your table, right this way.d
Why Does Validation and Error Handling Matter?
Because 1 invalid input without clear errors is how you get 1-star reviews from the QA team.
Centralized Exception Handling
package com.example.coffee.api;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Map<String, Object>> handleNotFound(NotFoundException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now().toString());
body.put("status", 404);
body.put("error", "Not Found");
body.put("message", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now().toString());
body.put("status", 400);
body.put("error", "Bad Request");
body.put("message", "Validation failed");
body.put("fields", ex.getBindingResult().getFieldErrors().stream()
.collect(java.util.stream.Collectors.toMap(
err -> err.getField(),
err -> err.getDefaultMessage(),
(a, b) -> a)));
return ResponseEntity.badRequest().body(body);
}
}
And a simple custom exception:
package com.example.coffee.api;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) { super(message); }
}
With this, you avoid spraying try/catch blocks inside your controllers. Your responses are consistent and helpful.
HTTP Method Mapping Cheatsheet (a gentle nudge from REST school)
| Mapping | Semantics | Idempotent | Typical Response |
|---|---|---|---|
@GetMapping |
Read resource(s) | Yes | 200 OK (body) or 404 if missing |
@PostMapping |
Create new resource | No | 201 Created + Location header |
@PutMapping |
Replace entire resource | Yes | 200 OK or 204 No Content |
@PatchMapping |
Partial update | Not guaranteed | 200 OK (body) |
@DeleteMapping |
Remove resource | Yes | 204 No Content (if existed) |
If youre updating a single field with PUT, somewhere a PATCH RFC sighs heavily.
Common Mistakes in Creating REST Controllers
- Returning 200 for a create operation. Use 201 +
Locationwhen you POST. - Forgetting
@RequestBodyon JSON input, resulting in mysterious415 Unsupported Media Typeor null fields. - Mixing view controllers and REST controllers. Keep
@Controllerand@RestControllerlanes separate. - Leaking domain exceptions as 500s. Map them intentionally (e.g., 404 for not found, 409 for conflicts).
- Ignoring validation. Add
@Validand return field errors like a civilized API. - Designing verbs in URIs (
/createCoffee). Use nouns and HTTP methods (/coffees). - Returning entities directly. Prefer DTOs to avoid overexposing internals and to control your API contract.
- Skipping pagination for list endpoints. Unbounded lists are fun until prod meets 1M rows.
Pro Tips That Make You Look Unreasonably Good
- Content negotiation is automatic. Still, you can be explicit:
@GetMapping(produces = "application/json"). - Use
ResponseEntitywhen you need surgical control over status or headers. - Keep paths kebab-case and plural:
/api/v1/coffeesnot/api/v1/CoffeeList. - Traceability: add correlation IDs via filters; your future debugging self will weep with gratitude.
- For PATCH, consider JSON Merge Patch or JSON Patch formats for clarity and safety.
Micro-Quest: Can You Spot Better Design?
Imagine this in your everyday life: you order a latte (POST), check your order status (GET), change your milk option (PATCH), or reorder the same thing (not idempotent unless your cafe is weird). Why do people keep misunderstanding this? Because we love verbs in URLs. Resist. Let the HTTP method do the talking.
Clean REST is like good UI: when its done right, nobody notices 1 they just get what they need without friction.
Quick Setup Reminder (because were building on earlier steps)
You already:
- Created a Spring Boot app (starter-web brings MVC + Jackson).
- Understood REST principles (resources, idempotency, status codes).
Now you:
- Add a controller class under a component-scanned package.
- Define mappings with clear URIs.
- Return DTOs with correct status codes.
- Centralize error handling with
@RestControllerAdvice.
Summary: Creating REST Controllers Without Chaos
Creating REST controllers in Spring Boot is where architecture meets reality. You map clean URIs to clear methods, honor HTTP semantics, validate inputs, and send back JSON like a pro. Keep @RestController for APIs, use DTOs, return the right status codes, and handle errors like you meant it.
Key takeaways:
- Controllers translate HTTP into your domain and back to JSON.
@RestController+ mapping annotations give you fast, clean endpoints.ResponseEntityand@RestControllerAdvicegive you control and consistency.- REST isnt just a vibe; its a contract. Follow it, and clients will love you.
Next up, youll wire this to persistence or call out to other microservices and really test those REST muscles. For now? Youve got the front door built. Its sturdy, stylish, and it returns 201 with a Location header. Chefs kiss.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!