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
React Frontend: Captures form data via
react-hook-form, performs initial validation, and sends an HTTPPOSTrequest with a JSON payload.Spring Boot Controller: Intercepts the request and triggers validation via
@Valid.DTO (Data Transfer Object): Enforces field rules using Jakarta Bean Validation annotations (
@NotBlank,@Email,@Size, etc.).Global Exception Handler: Catches any validation errors before business logic runs and returns a structured
Map<String, String>of field-level errors with HTTP status400 Bad Request.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:
<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:
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:
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:
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:
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
Post a Comment