WL
Software Engineer
Wassim Lagnaoui

Tutorial 04: Menu Items API

Controller, service, DTOs, and mapper for full menu management.

← Back to Tutorials

The menu items feature is a good reference implementation of a clean three-layer design: the controller handles HTTP, the service owns the business logic, and the mapper converts between the entity and the API shape. Everything flows through DTOs at the boundaries.


1. The Controller: HTTP layer only

The controller's only job is to translate HTTP verbs and paths into service calls, and wrap the result in a ResponseEntity. No business logic belongs here.

@RestController
@RequestMapping("/api/menu-items")
public class MenuItemController {

    private final MenuItemService menuItemService;

    public MenuItemController(MenuItemService menuItemService) {
        this.menuItemService = menuItemService;
    }

    @PostMapping("/add")
    public ResponseEntity<MenuItemResponse> createMenuItem(
            @RequestBody @Valid MenuItemRequest req) {
        MenuItemResponse response = menuItemService.createMenuItem(req);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<MenuItemResponse> getMenuItemByID(@PathVariable Long id) {
        return ResponseEntity.ok(menuItemService.findById(id));
    }

    @GetMapping
    public ResponseEntity<List<MenuItemResponse>> getAllMenuItems() {
        return ResponseEntity.ok(menuItemService.findAll());
    }

    @PutMapping("/{id}")
    public ResponseEntity<MenuItemResponse> updateAvailability(@PathVariable Long id) {
        return ResponseEntity.ok(menuItemService.updateAvailability(id));
    }

    @GetMapping("/available")
    public ResponseEntity<List<MenuItemResponse>> getAvailableMenuItems() {
        return ResponseEntity.ok(menuItemService.getAvailableMenuItems());
    }

    @GetMapping("/category/{category}")
    public ResponseEntity<List<MenuItemResponse>> getMenuItemsByCategory(
            @PathVariable String category) {
        return ResponseEntity.ok(menuItemService.getMenuItemsByCategory(category));
    }
}
  • @RestController: combines @Controller and @ResponseBody: every method return value is serialized to JSON automatically.
  • @RequestMapping("/api/menu-items"): sets the base path for all methods in the class.
  • @Valid on @RequestBody: triggers Bean Validation on the incoming DTO before the method body runs. Invalid fields throw a MethodArgumentNotValidException automatically.
  • ResponseEntity.status(CREATED): POST returns 201 Created: semantically more correct than 200 OK for resource creation.
  • Constructor injection (not @Autowired on the field) is the recommended style: it makes the dependency explicit and works without Spring context in tests.

2. The Service: business logic

Creating a menu item

@Slf4j
@Service
public class MenuItemService {

    private final MenuItemRepository menuItemRepository;

    public MenuItemService(MenuItemRepository menuItemRepository) {
        this.menuItemRepository = menuItemRepository;
    }

    public MenuItemResponse createMenuItem(MenuItemRequest req) {
        MenuItem item = MenuItemMapper.toMenuItem(req);  // request DTO → entity
        menuItemRepository.save(item);                   // persist (ID is generated)
        return MenuItemMapper.fromMenuItem(item);        // entity → response DTO
    }
}

Finding by ID with exception on miss

public MenuItemResponse findById(Long id) {
    MenuItem item = menuItemRepository.findById(id)
        .orElseThrow(() -> new MenuItemIdNotFoundException(id));
    return MenuItemMapper.fromMenuItem(item);
}

Toggling availability

public MenuItemResponse updateAvailability(Long id) {
    MenuItem item = menuItemRepository.findById(id)
        .orElseThrow(MenuItemNotAvailableException::new);

    item.setAvailable(!item.isAvailable()); // toggle the flag
    menuItemRepository.save(item);

    return MenuItemMapper.fromMenuItem(item);
}
  • orElseThrow: avoids null checks. The exception type determines the HTTP status: the global exception handler maps it to 404 or 400 automatically.
  • Toggle pattern: a single PUT endpoint handles both enable and disable. The client doesn't pass a value: it just sends the ID and the server flips the flag.
  • @Slf4j: injects a log field for logging without boilerplate. Use log.info() or log.warn() in the service for traceability in production.

Filtering by category

public List<MenuItemResponse> getMenuItemsByCategory(String category) {
    return menuItemRepository.findByCategory(category)
        .stream()
        .map(MenuItemMapper::fromMenuItem)
        .collect(Collectors.toList());
}
  • The stream pipeline replaces the manual for-loop: findByCategory returns entities, map converts each one, collect gathers the results.
  • Method reference MenuItemMapper::fromMenuItem is equivalent to item -> MenuItemMapper.fromMenuItem(item): shorter and clearer.

3. The full endpoint map

MethodPathPurpose
POST/api/menu-items/addCreate a new item (admin)
GET/api/menu-itemsList all items
GET/api/menu-items/{id}Get one item by ID
GET/api/menu-items/availableList only available items
GET/api/menu-items/category/{cat}Filter by category
PUT/api/menu-items/{id}Toggle availability
PUT/api/menu-items/{id}/priceUpdate price
PUT/api/menu-items/{id}/nameUpdate name

Key Takeaways

  • Controllers translate HTTP to service calls: validation, status codes, and response wrapping live here, nothing else.
  • @Valid on @RequestBody runs Bean Validation automatically before your method executes.
  • Always use orElseThrow instead of if (item == null): the exception type carries the semantic meaning and the global handler converts it to the right HTTP status.
  • The toggle pattern (single PUT that flips a boolean) avoids separate enable/disable endpoints and keeps the API surface small.
  • Stream + method reference is the idiomatic way to map a list of entities to DTOs: replace manual for-loops.