Business Scenario
In this lab, we will enhance the REST API by adding request validation, proper HTTP status codes, ResponseEntity, custom exceptions, and global exception handling.
Pre-Lab Preparation
Phase 3 — Validation + ResponseEntity + Exception Handling
Module:
1) Advance Spring data JPA and production ready Rest APi
Add Validation Dependency
1
Open: pom.xml Inside <dependencies>, add:
<!-- Used for validating request data -->
<dependency>
Create Exception package
2
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
com.medicare.exception
DoctorNotFoundException.java file empty. We will add its implementation in the next step.Add Validation to Doctor
3
Open Doctor.java and Update it to:
package com.medicare.entity;
import java.math.BigDecimal;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "doctors")
public class Doctor { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Doctor name is required")
@Size(min = 3, max = 100, message = "Doctor name must be between 3 and
100 characters")
@Column(nullable = false)
private String name;
@NotBlank(message = "Specialization is required")
@Size(max = 100, message = "Specialization cannot exceed 100 characters")
@Column(nullable = false)
private String specialization;
@NotNull(message = "Experience is required")
@Min(value = 0, message = "Experience cannot be negative")
@Max(value = 60, message = "Experience cannot exceed 60 years")
@Column(name = "experience_years", nullable = false)
private Integer experienceYears;
@NotNull(message = "Consultation fee is required")
@Positive(message = "Consultation fee must be greater than zero")
@Column(name = "consultation_fee", nullable = false, precision = 10, scale = 2)
private BigDecimal consultationFee;
@Size(max = 500, message = "Photo URL cannot exceed 500 characters")
@Column(name = "photo_url")
private String photoUrl; public Doctor() {
}
public Doctor(
String name,
String specialization,
Integer experienceYears,
BigDecimal consultationFee,
String photoUrl) {
this.name = name;
this.specialization = specialization;
this.experienceYears = experienceYears;
this.consultationFee = consultationFee;
this.photoUrl = photoUrl;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
public String getSpecialization() {
return specialization;
}
public void setSpecialization(String specialization) {
this.specialization = specialization;
}
public Integer getExperienceYears() {
return experienceYears;
}
public void setExperienceYears(Integer experienceYears) {
this.experienceYears = experienceYears;
}
public BigDecimal getConsultationFee() {
return consultationFee;
}
public void setConsultationFee(BigDecimal consultationFee) {
this.consultationFee = consultationFee;
} public String getPhotoUrl() {
return photoUrl;
}
public void setPhotoUrl(String photoUrl) {
this.photoUrl = photoUrl;
}
}Implement DoctorNotFoundException
4
package com.medicare.exception;
public class DoctorNotFoundException extends RuntimeException {
public DoctorNotFoundException(String message) {
super(message);
}
}Update DoctorService
5
package com.medicare.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.medicare.entity.Doctor;
import com.medicare.exception.DoctorNotFoundException;
import com.medicare.repository.DoctorRepository;
@Service
public class DoctorService {
private final DoctorRepository doctorRepository;
public DoctorService(DoctorRepository doctorRepository) {
this.doctorRepository = doctorRepository;
} public Doctor createDoctor(Doctor doctor) {
return doctorRepository.save(doctor);
}
public List<Doctor> getAllDoctors() { return doctorRepository.findAll(); }
public Doctor getDoctorById(Long id) { return findDoctorById(id); }
public Doctor updateDoctor(Long id, Doctor doctorDetails) {
Doctor existingDoctor = findDoctorById(id);
existingDoctor.setName(doctorDetails.getName());
existingDoctor.setSpecialization(doctorDetails.getSpecialization());
existingDoctor.setExperienceYears(doctorDetails.getExperienceYears());
existingDoctor.setConsultationFee(doctorDetails.getConsultationFee());
existingDoctor.setPhotoUrl(doctorDetails.getPhotoUrl());
return doctorRepository.save(existingDoctor);
}
public void deleteDoctor(Long id) {
Doctor doctor = findDoctorById(id);
doctorRepository.delete(doctor);
}
private Doctor findDoctorById(Long id) {
return doctorRepository.findById(id)
.orElseThrow(() ->
new DoctorNotFoundException("Doctor not found with id: " + id));
}
}Create ErrorResponse
6
com.medicare.response
package com.medicare.response;
import java.time.LocalDateTime;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String message;
private String path; public ErrorResponse(
LocalDateTime timestamp,
int status,
String message,
String path) {
this.timestamp = timestamp;
this.status = status;
this.message = message;
this.path = path;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
public String getPath() {
return path;
}
}{
"timestamp": "2026-09-17T14:30:00",
"status": 404,
"message": "Doctor not found with id: 50",
"path": "/api/doctors/50"
}Create Validation Error Response
7
Create ValidationErrorResponse.java inside the response package
package com.medicare.response;
import java.time.LocalDateTime;
import java.util.Map;public class ValidationErrorResponse {
private LocalDateTime timestamp;
private int status;
private String message;
private String path;
private Map<String, String> errors;
public ValidationErrorResponse(
LocalDateTime timestamp,
int status,
String message,
String path,
Map<String, String> errors) {
this.timestamp = timestamp;
this.status = status;
this.message = message;
this.path = path;
this.errors = errors;
}
public LocalDateTime getTimestamp() { return timestamp; }
public int getStatus() { return status; }
public String getMessage() { return message; }
public String getPath() { return path; }
public Map<String, String> getErrors() { return errors; }
}Create Global Exception Handler
8
Inside the exception package : Create GlobalExceptionHandler.java
package com.medicare.exception;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
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 com.medicare.response.ErrorResponse;
import com.medicare.response.ValidationErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
@RestControllerAdvice
public class GlobalExceptionHandler { @ExceptionHandler(DoctorNotFoundException.class)
public ResponseEntity<ErrorResponse> handleDoctorNotFound(
DoctorNotFoundException exception,
HttpServletRequest request) {
ErrorResponse errorResponse = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.NOT_FOUND.value(),
exception.getMessage(),
request.getRequestURI()
);
return ResponseEntity .status(HttpStatus.NOT_FOUND) .body(errorResponse);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(
MethodArgumentNotValidException exception,
HttpServletRequest request) {
Map<String, String> errors = new LinkedHashMap<>();
exception.getBindingResult()
.getFieldErrors()
.forEach(error -> errors.put(
error.getField(),
error.getDefaultMessage()
)); ValidationErrorResponse errorResponse = new ValidationErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
request.getRequestURI(),
errors
);
return ResponseEntity
.badRequest()
.body(errorResponse);
}
}Update DoctorController
9
package com.medicare.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medicare.entity.Doctor;
import com.medicare.service.DoctorService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/doctors")
public class DoctorController {
private final DoctorService doctorService;
public DoctorController(DoctorService doctorService) {
this.doctorService = doctorService;
}
@PostMapping
public ResponseEntity<Doctor> createDoctor(@Valid @RequestBody Doctor doctor) {
Doctor savedDoctor = doctorService.createDoctor(doctor);
return ResponseEntity.status(HttpStatus.CREATED).body(savedDoctor);
}
@GetMapping
public ResponseEntity<List<Doctor>> getAllDoctors() { List<Doctor> doctors = doctorService.getAllDoctors();
return ResponseEntity.ok(doctors);
}
@GetMapping("/{id}")
public ResponseEntity<Doctor> getDoctorById(@PathVariable Long id) {
Doctor doctor = doctorService.getDoctorById(id);
return ResponseEntity.ok(doctor);
}
@PutMapping("/{id}")
public ResponseEntity<Doctor> updateDoctor(@PathVariable Long id,
@Valid @RequestBody Doctor doctor) {
Doctor updatedDoctor = doctorService.updateDoctor(id, doctor);
return ResponseEntity.ok(updatedDoctor);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteDoctor(@PathVariable Long id) {
doctorService.deleteDoctor(id);
return ResponseEntity.noContent().build();
}
}Test Invalid Doctor
10
{
"name": "",
"specialization": "",
"experienceYears": -5,
"consultationFee": -100
}{
"timestamp": "2026-09-17T14:30:00",
"status": 400,
"message": "Validation failed",
"path": "/api/doctors",
"errors": {
"name": "Doctor name is required",
"specialization": "Specialization is required",
"experienceYears": "Experience cannot be negative",
"consultationFee": "Consultation fee must be greater than zero"
}
}Test Doctor That Doesn't Exist
11
{
"timestamp": "2026-09-17T14:35:00",
"status": 404,
"message": "Doctor not found with id: 9999",
"path": "/api/doctors/9999"
}Test Invalid UPDATE
12
{
"name": "",
"specialization": "", "experienceYears": -10, "consultationFee": 0 }
400 BAD REQUEST
Delete a Doctor That Doesn't Exist
13
{
"timestamp": "...",
"status": 404, "message": "Doctor not found with id: 1", "path": "/api/doctors/1" }
We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below:
Great job!
You have successfully enhanced the Medicare REST API with validation, ResponseEntity, proper HTTP status codes, and global exception handling.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) Advance Spring data JPA and production ready Rest APi