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());
    }
}
AnnotationPurpose
@RestController@Controller plus @ResponseBody: return values are serialised, JSON by default
@RequestMappingClass-level base path shared by every method
@PathVariableBinds a URI template segment to a parameter
@RequestParamBinds a query parameter; supports required and defaultValue
@RequestBodyDeserialises the request body into an object
@ValidRuns Bean Validation on the argument before the method body
@ResponseStatusSets the status code for a successful return
@ControllerAdviceCentralises 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 plain new and 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 @Primary or select with @Qualifier("name").
  • The stereotypes are descriptive: @Service, @Repository and @Component all register a bean, and @Repository also 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.

Project setup and your first controller Configuration, JPA data access and profiles

Last refreshed 2026-09-18.