REST controllers and dependency injection
The HTTP annotations you will use daily, DTOs at the boundary, and constructor injection done properly.
REST controllers
@RestController
@RequestMapping("/api/users")
class UserController {
private final UserService service;
UserController(UserService service) { this.service = service; }
@GetMapping // GET /api/users
List<UserDto> list() {
return service.all();
}
@GetMapping("/{id}") // GET /api/users/7
UserDto one(@PathVariable long id) {
return service.byId(id);
}
@PostMapping // POST /api/users
@ResponseStatus(HttpStatus.CREATED)
UserDto create(@Valid @RequestBody CreateUser body) {
return service.create(body);
}
@DeleteMapping("/{id}") // DELETE /api/users/7
void delete(@PathVariable long id) {
service.delete(id);
}
}record CreateUser(@NotBlank String name, @Email String email) {}
record UserDto(long id, String name, String email) {
static UserDto from(User u) {
return new UserDto(u.getId(), u.getName(), u.getEmail());
}
}| Annotation | Purpose |
|---|---|
@RestController | @Controller plus @ResponseBody: return values are serialised, JSON by default |
@RequestMapping | Class-level base path shared by every method |
@PathVariable | Binds a URI template segment to a parameter |
@RequestParam | Binds a query parameter; supports required and defaultValue |
@RequestBody | Deserialises the request body into an object |
@Valid | Runs Bean Validation on the argument before the method body |
@ResponseStatus | Sets the status code for a successful return |
@ControllerAdvice | Centralises exception to response mapping across controllers |
Dependency injection
@Service
class UserService {
private final UserRepository repo;
// a single constructor needs no @Autowired
UserService(UserRepository repo) {
this.repo = repo;
}
List<UserDto> all() {
return repo.findAll().stream().map(UserDto::from).toList();
}
}- Constructor injection makes dependencies explicit and
final, and lets tests build the class with plainnewand no container. - Beans are singletons by default. Avoid mutable instance fields unless the state is thread-safe or explicitly request another scope.
- Inject; do not look up. Calling
ApplicationContext.getBean()inside business code hides dependencies and defeats the container. - When several beans implement one interface, mark one
@Primaryor select with@Qualifier("name"). - The stereotypes are descriptive:
@Service,@Repositoryand@Componentall register a bean, and@Repositoryalso adds persistence exception translation.
⚠️
Field injection with
@Autowired hides dependencies, blocks immutability and makes tests need reflection. Use constructor injection, and keep controllers thin: validation and mapping in the web layer, business rules in the service.FAQ
Should I return the entity or a DTO?
Return a DTO. Entities carry lazy relations and persistence annotations that leak into the API contract, and serialising a lazy relation outside a transaction throws.
Where should @Transactional go?
On the service method that defines the unit of work — not on the controller, and rarely on the repository. Keep transactions short and free of remote calls.
Related
Project setup and your first controller Configuration, JPA data access and profiles
Last refreshed 2026-09-18.