WL
Software Engineer
Wassim Lagnaoui

Tutorial 05: Staff API

Managing staff records and tying employee IDs to the registration flow.

← Back to Tutorials

The Staff API serves two purposes: it lets admins manage who works at the restaurant, and it acts as a gate for user registration. Only people with a valid employee ID in the staff table can create an account. This tutorial walks through the API and explains the design decision behind that gate.


1. The Staff entity and repository

The Staff entity is reference data: it represents real people who work at the restaurant. It is managed separately from User (the auth entity) and linked by employeeId.

// Staff fields: firstName, lastName, email, employeeId (Long, unique), role (String)
// Example roles: "waiter", "chef", "manager", "admin"

@Repository
public interface StaffRepository extends JpaRepository<Staff, Long> {
    Optional<Staff> findByEmployeeId(Long employeeId);
}
  • findByEmployeeId is derived automatically by Spring Data: no @Query needed.
  • Returns Optional instead of Staff to make the "not found" case explicit at the call site.
  • The role field is a plain String here (not an enum). This keeps the staff table flexible: roles can be changed without a schema migration.

2. DTOs: request and response shapes

AddStaffDTO (request)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AddStaffDTO {
    private String firstName;
    private String lastName;
    private String email;
    private Long employeeId;  // must be unique: this is the registration key
    private String role;      // e.g. "waiter", "chef", "manager"
}

StaffInfoDTO (response)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StaffInfoDTO {
    private String firstName;
    private String lastName;
    private String email;
    private Long employeeId;
    private String role;
}
  • The request and response shapes happen to look the same here. In practice, the response would exclude internal fields (like the DB primary key) while the request excludes auto-generated fields.
  • The staffAddedResponse (returned by the add endpoint) is a simple confirmation: message + the assigned employeeId.

3. Controller: two endpoints

@RestController
@RequestMapping("/api/staff")
public class StaffController {

    private final StaffManagementService staffManagementService;

    public StaffController(StaffManagementService staffManagementService) {
        this.staffManagementService = staffManagementService;
    }

    @PostMapping("/add")
    public ResponseEntity<staffAddedResponse> addStaff(@RequestBody @Valid AddStaffDTO dto) {
        staffAddedResponse response = staffManagementService.addStaff(dto);
        return ResponseEntity.ok(response);
    }

    @GetMapping("/all")
    public ResponseEntity<List<StaffInfoDTO>> getAllStaff() {
        List<StaffInfoDTO> staffList = staffManagementService.getAllStaff();
        return ResponseEntity.ok(staffList);
    }
}
  • Both endpoints are protected by the security config: only authenticated admin users can reach /api/staff.
  • The class name staffAddedResponse uses lowercase: this is a naming inconsistency in the actual project. Convention is PascalCase for class names.

4. Service: add staff and list staff

@Service
public class StaffManagementService {

    private final StaffRepository staffRepository;

    public StaffManagementService(StaffRepository staffRepository) {
        this.staffRepository = staffRepository;
    }

    public staffAddedResponse addStaff(AddStaffDTO dto) {
        Staff staff = new Staff();
        staff.setFirstName(dto.getFirstName());
        staff.setLastName(dto.getLastName());
        staff.setEmail(dto.getEmail());
        staff.setEmployeeId(dto.getEmployeeId());
        staff.setRole(dto.getRole());

        Staff saved = staffRepository.save(staff);
        return new staffAddedResponse("Staff member added successfully", saved.getEmployeeId());
    }

    public List<StaffInfoDTO> getAllStaff() {
        return staffRepository.findAll().stream()
            .map(s -> new StaffInfoDTO(
                s.getFirstName(),
                s.getLastName(),
                s.getEmail(),
                s.getEmployeeId(),
                s.getRole()
            ))
            .collect(Collectors.toList());
    }
}
  • No mapper class is used here: the service maps inline. This is fine for simple cases; extract a StaffMapper if the mapping grows complex.
  • The stream-to-DTO pattern (.stream().map(...).collect()) is the same approach used in the menu service: consistent across the codebase.

5. The employee ID gate: why registration checks staff

The registration endpoint in AuthService doesn't just create a user: it first verifies that the submitted employeeId exists in the Staff table:

// Inside AuthService.register()
public AuthResponse register(RegisterRequestDTO req) {
    boolean staffExists = staffRepository.findByEmployeeId(req.getEmployeeId()).isPresent();

    if (!staffExists) {
        throw new UsernameNotFoundException(
            "Staff with Employee ID " + req.getEmployeeId() + " does not exist."
        );
    }

    User user = User.builder()
        .email(req.getEmail())
        .password(passwordEncoder.encode(req.getPassword()))
        .name(req.getName())
        .role(Role.ROLE_USER)
        .build();

    userRepository.save(user);
    return new AuthResponse(jwtService.generateToken(user));
}
  • Why: the restaurant system is internal: only actual employees should be able to create accounts. A public registration form without this check would let anyone sign up.
  • Flow: an admin adds a staff member first (POST /api/staff/add), giving them an employeeId. The staff member then registers with that ID. Without it, registration is rejected.
  • New users are always created with ROLE_USER: an admin must manually elevate them if admin access is needed. This is a safe default.

Key Takeaways

  • The Staff table is reference data, separate from User (the auth table). They are linked only by employeeId during registration.
  • The employee ID gate prevents open registration. Only people an admin has pre-registered as staff can create accounts.
  • New users always get ROLE_USER. Admin access is granted separately: never by default.
  • Simple mappings (few fields, no nesting) can be done inline in the service. Extract a mapper class when the complexity grows.
  • Returning an Optional from the repository makes the "not found" scenario impossible to ignore: you must handle it at the call site.