<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sulochana Basavala</title>
    <description>The latest articles on DEV Community by Sulochana Basavala (@sulochana_basavala).</description>
    <link>https://dev.to/sulochana_basavala</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4187304%2F49043311-6617-4253-93e4-04c6e6b0ea21.jpg</url>
      <title>DEV Community: Sulochana Basavala</title>
      <link>https://dev.to/sulochana_basavala</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sulochana_basavala"/>
    <language>en</language>
    <item>
      <title>Securing React and Spring Boot with JWT &amp; Refresh Tokens (Spring Security 6)</title>
      <dc:creator>Sulochana Basavala</dc:creator>
      <pubDate>Fri, 16 Oct 2026 17:35:32 +0000</pubDate>
      <link>https://dev.to/sulochana_basavala/securing-react-and-spring-boot-with-jwt-refresh-tokens-spring-security-6-24b3</link>
      <guid>https://dev.to/sulochana_basavala/securing-react-and-spring-boot-with-jwt-refresh-tokens-spring-security-6-24b3</guid>
      <description>&lt;p&gt;In modern full-stack development, securing communication between a Single Page Application (SPA) and a REST API is a fundamental requirement. While stateless JSON Web Tokens (JWTs) are popular, relying on a single, long-lived token introduces a major vulnerability: if intercepted, an attacker retains access until expiration with no immediate revocation mechanism.&lt;/p&gt;

&lt;p&gt;Shortening token validity to 15 minutes resolves this exposure, but causes abrupt logouts unless automated.&lt;/p&gt;

&lt;p&gt;The standard architectural pattern is a &lt;strong&gt;dual-token lifecycle&lt;/strong&gt;: short-lived Access Tokens paired with longer-lived Refresh Tokens and automated silent interceptors.&lt;/p&gt;

&lt;p&gt;In this tutorial, we will build a production-grade &lt;strong&gt;dual-token authentication system&lt;/strong&gt; using &lt;strong&gt;Spring Boot 3 (Spring Security 6)&lt;/strong&gt; and &lt;strong&gt;React with Axios Interceptors&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Dual-Token Architecture Overview
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access Token (15 Minutes):&lt;/strong&gt; Passed in the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header on every HTTP request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh Token (7 Days):&lt;/strong&gt; Stored securely and sent only to &lt;code&gt;/api/auth/refresh&lt;/code&gt; when an access token expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent Renewal:&lt;/strong&gt; The React client catches &lt;code&gt;401 Unauthorized&lt;/code&gt; responses via an Axios response interceptor, fetches a fresh access token, and automatically retries the initial request seamlessly.&lt;/li&gt;
&lt;/ol&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
text
[ React Client (Axios) ]                       [ Spring Boot 3 Backend ]
         |                                                 |
         | ----- POST /api/auth/login -------------------&amp;gt; | (Authenticates credentials)
         | &amp;lt;---- 200 OK { accessToken, refreshToken } ---- |
         |                                                 |
         | ----- GET /api/dashboard (Bearer AccessToken)-&amp;gt; | (Validates signature &amp;amp; expiry)
         | &amp;lt;---- 200 OK { protected data } --------------- |
         |                                                 |
   (15 min passes: Access Token expires)                   |
         |                                                 |
         | ----- GET /api/dashboard (Expired Token) -----&amp;gt; |
         | &amp;lt;---- 401 Unauthorized ------------------------ |
         |                                                 |
   (Axios Interceptor catches 401)                         |
         | ----- POST /api/auth/refresh (RefreshToken) --&amp;gt; | (Validates refresh token)
         | &amp;lt;---- 200 OK { accessToken: newAccessToken } -- |
         |                                                 |
   (Interceptor retries original request)                  |
         | ----- GET /api/dashboard (New AccessToken) ---&amp;gt; |
         | &amp;lt;---- 200 OK { protected data } --------------- |
2. Spring Boot 3 Backend Setup
Maven Dependencies (pom.xml)
XML
&amp;lt;dependencies&amp;gt;
    &amp;lt;!-- Spring Security &amp;amp; Web --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;

    &amp;lt;!-- Java JWT (JJWT) 0.12.x --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt-api&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.12.5&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt-impl&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.12.5&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt-jackson&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.12.5&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
Step 1: JWT Service (JwtService.java)
Manages signing, claims extraction, and expiration validation:

Java
package com.example.demo.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtService {

    // 256-bit secret key (Keep this secure in environment variables in production)
    private static final String SECRET = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970";

    private static final long ACCESS_TOKEN_EXPIRATION = 15 * 60 * 1000;       // 15 minutes
    private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7 days

    private SecretKey getSignInKey() {
        return Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(UserDetails userDetails) {
        return buildToken(new HashMap&amp;lt;&amp;gt;(), userDetails.getUsername(), ACCESS_TOKEN_EXPIRATION);
    }

    public String generateRefreshToken(UserDetails userDetails) {
        return buildToken(new HashMap&amp;lt;&amp;gt;(), userDetails.getUsername(), REFRESH_TOKEN_EXPIRATION);
    }

    private String buildToken(Map&amp;lt;String, Object&amp;gt; extraClaims, String subject, long expirationMs) {
        return Jwts.builder()
                .claims(extraClaims)
                .subject(subject)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expirationMs))
                .signWith(getSignInKey())
                .compact();
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) &amp;amp;&amp;amp; !isTokenExpired(token));
    }

    private boolean isTokenExpired(String token) {
        return extractClaim(token, Claims::getExpiration).before(new Date());
    }

    public &amp;lt;T&amp;gt; T extractClaim(String token, Function&amp;lt;Claims, T&amp;gt; claimsResolver) {
        final Claims claims = Jwts.parser()
                .verifyWith(getSignInKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
        return claimsResolver.apply(claims);
    }
}
Step 2: The Authentication Filter (JwtAuthenticationFilter.java)
Intercepts incoming requests and validates the bearer token before Spring Security authorizes the request:

Java
package com.example.demo.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userEmail;

        try {
            userEmail = jwtService.extractUsername(jwt);
        } catch (Exception e) {
            filterChain.doFilter(request, response);
            return;
        }

        if (userEmail != null &amp;amp;&amp;amp; SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);

            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}
Step 3: Security Configuration (SecurityConfig.java)
Java
package com.example.demo.config;

import com.example.demo.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter) {
        this.jwtAuthFilter = jwtAuthFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -&amp;gt; cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -&amp;gt; csrf.disable())
            .authorizeHttpRequests(auth -&amp;gt; auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
Step 4: Auth Controller (AuthController.java)
Java
package com.example.demo.controller;

import com.example.demo.security.JwtService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final JwtService jwtService;

    public AuthController(AuthenticationManager authenticationManager,
                          UserDetailsService userDetailsService,
                          JwtService jwtService) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtService = jwtService;
    }

    @PostMapping("/login")
    public ResponseEntity&amp;lt;?&amp;gt; login(@RequestBody Map&amp;lt;String, String&amp;gt; request) {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.get("username"), request.get("password"))
        );

        UserDetails user = userDetailsService.loadUserByUsername(request.get("username"));
        String accessToken = jwtService.generateAccessToken(user);
        String refreshToken = jwtService.generateRefreshToken(user);

        return ResponseEntity.ok(Map.of(
                "accessToken", accessToken,
                "refreshToken", refreshToken
        ));
    }

    @PostMapping("/refresh")
    public ResponseEntity&amp;lt;?&amp;gt; refreshToken(@RequestBody Map&amp;lt;String, String&amp;gt; request) {
        String refreshToken = request.get("refreshToken");
        String username = jwtService.extractUsername(refreshToken);
        UserDetails user = userDetailsService.loadUserByUsername(username);

        if (jwtService.isTokenValid(refreshToken, user)) {
            String newAccessToken = jwtService.generateAccessToken(user);
            return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
        }

        return ResponseEntity.status(401).body(Map.of("message", "Invalid or expired refresh token"));
    }
}
3. React Frontend Integration
The Axios Silent Refresh Interceptor (src/api/axios.js)
JavaScript
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:8080/api',
});

// Attach access token to outgoing requests
api.interceptors.request.use(
  (config) =&amp;gt; {
    const accessToken = localStorage.getItem('accessToken');
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) =&amp;gt; Promise.reject(error)
);

// Intercept 401 Unauthorized and refresh silently
api.interceptors.response.use(
  (response) =&amp;gt; response,
  async (error) =&amp;gt; {
    const originalRequest = error.config;

    if (error.response?.status === 401 &amp;amp;&amp;amp; !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        if (!refreshToken) {
          throw new Error('No refresh token available');
        }

        const res = await axios.post('http://localhost:8080/api/auth/refresh', {
          refreshToken,
        });

        const newAccessToken = res.data.accessToken;
        localStorage.setItem('accessToken', newAccessToken);

        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return api(originalRequest);
      } catch (refreshError) {
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default api;
Critical Full-Stack Edge Cases
Avoid Infinite Loops on 401: Ensure originalRequest._retry = true is set before invoking the refresh endpoint to prevent recursive request loops.

Environment Variable Storage: Never commit signing keys directly to GitHub. Load secrets via environment variables (System.getenv(...)).

CORS on Security Blocks: Apply CORS configurations globally to avoid browsers masking 401 Unauthorized responses with generic cross-origin exceptions.

Originally published on my blog: Securing React and Spring Boot with JWT &amp;amp; Refresh Tokens.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>springboot</category>
      <category>react</category>
      <category>java</category>
      <category>security</category>
    </item>
    <item>
      <title>Connecting React Hook Form to Spring Boot REST API: Clean DTO Validation &amp; Global Exception Handling</title>
      <dc:creator>Sulochana Basavala</dc:creator>
      <pubDate>Thu, 15 Oct 2026 18:21:37 +0000</pubDate>
      <link>https://dev.to/sulochana_basavala/connecting-react-hook-form-to-spring-boot-rest-api-clean-dto-validation-global-exception-handling-541n</link>
      <guid>https://dev.to/sulochana_basavala/connecting-react-hook-form-to-spring-boot-rest-api-clean-dto-validation-global-exception-handling-541n</guid>
      <description>&lt;p&gt;In modern full-stack development, client-side validation gives users instant feedback, but it must never be your only line of defense. Client checks can easily be bypassed using tools like cURL or Postman.&lt;/p&gt;

&lt;p&gt;A secure, production-grade application requires robust server-side validation paired with clean, structured error feedback to the client.&lt;/p&gt;

&lt;p&gt;In this tutorial, we will connect a &lt;strong&gt;React Hook Form&lt;/strong&gt; frontend to a &lt;strong&gt;Spring Boot REST API&lt;/strong&gt; using &lt;strong&gt;Jakarta Bean Validation (&lt;code&gt;@Valid&lt;/code&gt;)&lt;/strong&gt;, centralize error handling with &lt;strong&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;&lt;/strong&gt;, and dynamically map backend field errors into React Hook Form using &lt;code&gt;setError()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
text
[ React Hook Form ]                         [ Spring Boot Backend ]
        |                                              |
        | --- POST /api/users (Payload) -------------&amp;gt; | (Validates DTO via @Valid)
        |                                              |
        |                                         [ Validation Fails ]
        |                                              |
        |                                         MethodArgumentNotValidException
        |                                              |
        |                                         @RestControllerAdvice
        |                                              |
        | &amp;lt;--- 400 Bad Request { field: message } ---- |
        |                                              |
(Calls setError() to display messages)
2. Spring Boot 3 Backend Setup
Dependencies (pom.xml)
Ensure you have the Spring Boot Validation starter:

XML
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-validation&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
The Request DTO (UserRegistrationDto.java)
Define validation rules using standard Jakarta constraints:

Java
package com.example.demo.dto;

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

public class UserRegistrationDto {

    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
    private String username;

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

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters long")
    private String password;

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

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

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
}
Centralized Error Handler (GlobalExceptionHandler.java)
Intercept MethodArgumentNotValidException and transform the validation errors into a clean key-value map (fieldName: errorMessage):

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&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; handleValidationExceptions(
            MethodArgumentNotValidException ex) {

        Map&amp;lt;String, String&amp;gt; errors = new HashMap&amp;lt;&amp;gt;();

        ex.getBindingResult().getAllErrors().forEach((error) -&amp;gt; {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        return new ResponseEntity&amp;lt;&amp;gt;(errors, HttpStatus.BAD_REQUEST);
    }
}
The Controller (UserController.java)
Use @Valid to enforce validation before controller logic runs:

Java
package com.example.demo.controller;

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

import java.util.Map;

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {

    @PostMapping
    public ResponseEntity&amp;lt;?&amp;gt; registerUser(@Valid @RequestBody UserRegistrationDto userDto) {
        return ResponseEntity.ok(Map.of("message", "User registered successfully!"));
    }
}
3. React Frontend Integration
Install Axios and React Hook Form:

Bash
npm install axios react-hook-form
Registration Form Component (RegistrationForm.jsx)
When an HTTP 400 response is returned, iterate through the server error object and assign each error message to its corresponding field with setError:

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

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

  const onSubmit = async (data) =&amp;gt; {
    try {
      const response = await axios.post('http://localhost:8080/api/users', data);
      alert(response.data.message);
      reset();
    } catch (err) {
      if (err.response &amp;amp;&amp;amp; err.response.status === 400) {
        const serverErrors = err.response.data;

        // Map backend errors dynamically into React Hook Form
        Object.keys(serverErrors).forEach((field) =&amp;gt; {
          setError(field, {
            type: 'server',
            message: serverErrors[field],
          });
        });
      } else {
        alert('An unexpected error occurred. Please try again.');
      }
    }
  };

  return (
    &amp;lt;div style={{ maxWidth: '400px', margin: '40px auto', fontFamily: 'sans-serif' }}&amp;gt;
      &amp;lt;h2&amp;gt;Create an Account&amp;lt;/h2&amp;gt;
      &amp;lt;form onSubmit={handleSubmit(onSubmit)} noValidate&amp;gt;

        {/* Username */}
        &amp;lt;div style={{ marginBottom: '16px' }}&amp;gt;
          &amp;lt;label style={{ display: 'block', marginBottom: '4px' }}&amp;gt;Username&amp;lt;/label&amp;gt;
          &amp;lt;input
            type="text"
            {...register('username')}
            style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
          /&amp;gt;
          {errors.username &amp;amp;&amp;amp; (
            &amp;lt;span style={{ color: 'red', fontSize: '12px' }}&amp;gt;{errors.username.message}&amp;lt;/span&amp;gt;
          )}
        &amp;lt;/div&amp;gt;

        {/* Email */}
        &amp;lt;div style={{ marginBottom: '16px' }}&amp;gt;
          &amp;lt;label style={{ display: 'block', marginBottom: '4px' }}&amp;gt;Email&amp;lt;/label&amp;gt;
          &amp;lt;input
            type="email"
            {...register('email')}
            style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
          /&amp;gt;
          {errors.email &amp;amp;&amp;amp; (
            &amp;lt;span style={{ color: 'red', fontSize: '12px' }}&amp;gt;{errors.email.message}&amp;lt;/span&amp;gt;
          )}
        &amp;lt;/div&amp;gt;

        {/* Password */}
        &amp;lt;div style={{ marginBottom: '16px' }}&amp;gt;
          &amp;lt;label style={{ display: 'block', marginBottom: '4px' }}&amp;gt;Password&amp;lt;/label&amp;gt;
          &amp;lt;input
            type="password"
            {...register('password')}
            style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
          /&amp;gt;
          {errors.password &amp;amp;&amp;amp; (
            &amp;lt;span style={{ color: 'red', fontSize: '12px' }}&amp;gt;{errors.password.message}&amp;lt;/span&amp;gt;
          )}
        &amp;lt;/div&amp;gt;

        &amp;lt;button
          type="submit"
          disabled={isSubmitting}
          style={{
            padding: '10px 16px',
            backgroundColor: '#0070f3',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
          }}
        &amp;gt;
          {isSubmitting ? 'Submitting...' : 'Register'}
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
Key Takeaways
DTO-Level Validation: Applying validation annotations directly to DTOs decouples business rules from database entity models.

Standardized Payloads: Returning a { fieldName: errorMessage } map keeps the API contract clean and straightforward to consume.

Seamless UI Integration: React Hook Form’s setError() updates the form state directly without manual re-render workarounds or custom state arrays.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>springboot</category>
      <category>react</category>
      <category>java</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
