Validation, ResponseEntity & Exception Handling

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>

  • Create package:

com.medicare.exception

  • Inside the package create: DoctorNotFoundException.java
  • For now, keep the 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;
    }
  • Previously we returned: .orElse(null);
  • This isn't ideal. Now we'll throw our proper exception.
  • Replace DoctorService.java with:
    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

  • Create package:

com.medicare.response

  • Inside the package create: ErrorResponse.java
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; 
    }
}
  • Instead of returning: Doctor not found we can return a meaningful JSON response:
{
    "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

  • Replace the controller with:
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

  • Now intentionally send bad data:
{
    "name": "",
    "specialization": "",
    "experienceYears": -5,
    "consultationFee": -100
}
  • Response should look similar to:
{
    "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"
    }
}
  • Now our API doesn't blindly accept bad data.

Test Doctor That Doesn't Exist

11

  • Now:GET http://localhost:8080/api/doctors/9999
  • The service executes: .orElseThrow(...) which throws: DoctorNotFoundException and the global handler catches it.
  • Response should look similar to:
{
    "timestamp": "2026-09-17T14:35:00",
    "status": 404,
    "message": "Doctor not found with id: 9999",
    "path": "/api/doctors/9999"
}

Test Invalid UPDATE

12

  • Try :
{
    "name": "",

    "specialization": "",
    "experienceYears": -10,
    "consultationFee": 0
}
  • Expected:

400 BAD REQUEST

Delete a Doctor That Doesn't Exist

13

  • Expected body:
{
    "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 branchName

Next-Lab Preparation

Module:

1) Advance Spring data JPA and production ready Rest APi​