Connecting React Hook Form with Spring Boot REST API & Global Validation

 

Connecting React Hook Form to Spring Boot: Clean DTO Validation & Global Exception Handling

In our previous tutorial https://sulochanabasavala.blogspot.com/2026/10/advanced-react-hook-form-custom-async-validation.html, we built custom synchronous and asynchronous validation pipelines on the frontend using React Hook Form.

While client-side validation provides immediate feedback, frontend validation can never replace server-side validation. Users can bypass UI rules via direct API calls, browser dev tools, or automation scripts.

In this guide, we will connect our React form to a robust Spring Boot REST API using Java Bean Validation (@Valid) and capture errors using a centralized @RestControllerAdvice (MethodArgumentNotValidException).

1. The Architecture: End-to-End Flow

  1. React Frontend: Captures form data via react-hook-form, performs initial validation, and sends an HTTP POST request with a JSON payload.

  2. Spring Boot Controller: Intercepts the request and triggers validation via @Valid.

  3. DTO (Data Transfer Object): Enforces field rules using Jakarta Bean Validation annotations (@NotBlank, @Email, @Size, etc.).

  4. Global Exception Handler: Catches any validation errors before business logic runs and returns a structured Map<String, String> of field-level errors with HTTP status 400 Bad Request.

  5. React Form: Maps server-side error keys directly back to input fields using setError().

2. Spring Boot Setup & Dependencies

Ensure you have the validation starter in your pom.xml:

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. Creating the Validated DTO

Define an EmployeeRequestDTO that mirrors the frontend form schema. Use standard Jakarta constraints to guard input integrity:

Java
package com.example.demo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public class EmployeeRequestDTO {

    @NotBlank(message = "First name is mandatory")
    @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
    private String firstName;

    @NotBlank(message = "Last name is mandatory")
    private String lastName;

    @NotBlank(message = "Email is mandatory")
    @Email(message = "Please provide a valid corporate email address")
    private String email;

    @NotBlank(message = "Mobile number is mandatory")
    @Pattern(regexp = "^[0-9]{10}$", message = "Mobile number must be a valid 10-digit number")
    private String mobile;

    @NotBlank(message = "Department selection is mandatory")
    private String department;

    // Constructors, Getters, and Setters
    public EmployeeRequestDTO() {}

    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }

    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getMobile() { return mobile; }
    public void setMobile(String mobile) { this.mobile = mobile; }

    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }
}

4. Building the REST Controller

Mark the @RequestBody with @Valid. If validation fails, Spring automatically aborts execution before the method body runs:

Java
package com.example.demo.controller;

import com.example.demo.dto.EmployeeRequestDTO;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/employees")
@CrossOrigin(origins = "http://localhost:3000") // Allow React dev server
public class EmployeeController {

    @PostMapping("/register")
    public ResponseEntity<?> registerEmployee(@Valid @RequestBody EmployeeRequestDTO request) {
        // Business logic execution (e.g., saving to database via JPA Service)
        Map<String, String> response = new HashMap<>();
        response.put("status", "SUCCESS");
        response.put("message", "Employee registered successfully!");
        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }
}

5. Global Exception Handling with MethodArgumentNotValidException

When validation fails on a @Valid parameter, Spring raises a MethodArgumentNotValidException. Rather than returning an unformatted 500 error or default Spring stack trace, intercept it with @RestControllerAdvice:

Java
package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {

        Map<String, String> errors = new HashMap<>();
        
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        // Returns HTTP 400 with a clean key-value map: {"email": "Please provide a valid...", ...}
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

6. Integrating with React Hook Form

In your React component, handle the 400 Bad Request response by setting field errors directly onto the form state using setError:

JavaScript
import React from 'react';
import { useForm } from 'react-hook-form';

export default function EmployeeForm() {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors, isSubmitting },
  } = useForm();

  const onSubmit = async (data) => {
    try {
      const response = await fetch('http://localhost:8080/api/employees/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        if (response.status === 400) {
          const serverErrors = await response.json();
          
          // Dynamically map backend validation errors to React Hook Form inputs
          Object.keys(serverErrors).forEach((field) => {
            setError(field, {
              type: 'server',
              message: serverErrors[field],
            });
          });
        } else {
          alert('Something went wrong on the server.');
        }
        return;
      }

      const result = await response.json();
      alert(result.message);
    } catch (err) {
      console.error('Network error:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="employee-form">
      <div>
        <label>First Name</label>
        <input {...register('firstName')} />
        {errors.firstName && <span className="error">{errors.firstName.message}</span>}
      </div>

      <div>
        <label>Email</label>
        <input {...register('email')} />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <label>Mobile Number</label>
        <input {...register('mobile')} />
        {errors.mobile && <span className="error">{errors.mobile.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Register Employee'}
      </button>
    </form>
  );
}

Conclusion

Frontend validation with React Hook Form provides an instant, responsive user experience, but robust server-side validation is non-negotiable for production security.

By combining Jakarta Bean Validation (@Valid) at the DTO level with a centralized @RestControllerAdvice, you prevent invalid payloads from reaching your business logic while returning clean, field-specific error maps. Handling those structured responses directly on the client with React Hook Form’s setError() ensures seamless end-to-end error feedback without unnecessary re-renders.

Key Takeaways:

  • Security & Integrity: Never trust client input alone; mirror validation rules across both the UI schema and backend DTOs.

  • Declarative Error Mapping: Standardize backend error responses as key-value pairs (fieldName: errorMessage) to map effortlessly into React form states.

  • Separation of Concerns: Keep your controller endpoints lightweight by delegating error serialization to a global exception handler.

In our next tutorial, we will explore Securing React + Spring Boot APIs with JWT & Refresh Tokens, covering authentication flow, protected routes, and seamless token refresh interceptors. Stay tuned!

Comments

Popular posts from this blog

React Basics – How to Create Component

How to Build REST API using Spring Boot (Step-by-Step Guide)

Spring Boot @RequestParam Annotation Explained with Real-Time Examples