reactspring-boottutorialfull-stack

React + Spring Boot: How to Connect Frontend and Backend (Complete Guide)

Siva Prasad Galaba· Staff Engineer at Crunchyroll | Founder, CodeBegun·

Step-by-step guide to connecting a React frontend with a Spring Boot REST API. Covers CORS, Axios, environment variables, API calls, and error handling.

You've built a Spring Boot REST API. You've built a React frontend. Now you need them to talk to each other — and this is where most beginners get stuck. CORS errors, 404s, environment variables — let me walk you through the complete integration in one guide.

Architecture Overview

Browser
  ↓ HTTP request
React App (Vite dev server: localhost:5173)
  ↓ API call (fetch/Axios)
Spring Boot API (localhost:8080)
  ↓ SQL query
MySQL Database (localhost:3306)

In development, React and Spring Boot run on different ports — this triggers CORS (Cross-Origin Resource Sharing) restrictions. In production, you either serve React as static files from Spring Boot, or use a reverse proxy (Nginx).

Step 1 — Configure CORS in Spring Boot

Spring Boot needs to explicitly allow requests from your React app's origin:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:5173", "https://your-domain.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true);
    }
}

Or use @CrossOrigin on individual controllers:

@RestController
@RequestMapping("/api/students")
@CrossOrigin(origins = "http://localhost:5173")
public class StudentController { ... }

After adding this, restart Spring Boot. The CORS error in your browser should be gone.

Step 2 — Install Axios in React

While you can use the native fetch API, Axios is cleaner for API integration:

npm install axios

Create an Axios instance with your base URL:

src/api/client.js

import axios from "axios";

const client = axios.create({
  baseURL: import.meta.env.VITE_API_URL || "http://localhost:8080",
  headers: {
    "Content-Type": "application/json",
  },
});

export default client;

src/api/students.js

import client from "./client";

export const studentApi = {
  getAll: () => client.get("/api/students"),
  getById: (id) => client.get(`/api/students/${id}`),
  create: (data) => client.post("/api/students", data),
  update: (id, data) => client.put(`/api/students/${id}`, data),
  delete: (id) => client.delete(`/api/students/${id}`),
};

Step 3 — Fetch Data in React

src/pages/StudentList.jsx

import { useState, useEffect } from "react";
import { studentApi } from "../api/students";

function StudentList() {
  const [students, setStudents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    studentApi.getAll()
      .then(res => {
        setStudents(res.data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.response?.data?.message || "Failed to load students");
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p style={{ color: "red" }}>{error}</p>;

  return (
    <div>
      <h2>Students ({students.length})</h2>
      {students.map(s => (
        <div key={s.id} style={{ padding: "12px", borderBottom: "1px solid #eee" }}>
          <strong>{s.name}</strong> — {s.city} — {s.course}
        </div>
      ))}
    </div>
  );
}

export default StudentList;

Step 4 — Create a Student (POST Request)

import { useState } from "react";
import { studentApi } from "../api/students";

function AddStudentForm({ onSuccess }) {
  const [form, setForm] = useState({ name: "", email: "", course: "", city: "" });
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState(null);

  function handleChange(e) {
    setForm({ ...form, [e.target.name]: e.target.value });
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setSubmitting(true);
    setError(null);

    try {
      await studentApi.create(form);
      setForm({ name: "", email: "", course: "", city: "" });
      onSuccess?.(); // callback to refresh the list
    } catch (err) {
      setError(err.response?.data?.message || "Failed to add student");
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "12px", maxWidth: "400px" }}>
      <input name="name" placeholder="Name" value={form.name} onChange={handleChange} required />
      <input name="email" type="email" placeholder="Email" value={form.email} onChange={handleChange} required />
      <input name="course" placeholder="Course" value={form.course} onChange={handleChange} />
      <input name="city" placeholder="City" value={form.city} onChange={handleChange} />
      {error && <p style={{ color: "red" }}>{error}</p>}
      <button type="submit" disabled={submitting}>
        {submitting ? "Adding..." : "Add Student"}
      </button>
    </form>
  );
}

Step 5 — Environment Variables

Don't hardcode API URLs. Use environment variables:

.env.development (React root, gitignored):

VITE_API_URL=http://localhost:8080

.env.production:

VITE_API_URL=https://api.your-domain.com

Vite exposes env variables prefixed with VITE_ to your code:

const baseURL = import.meta.env.VITE_API_URL;

Create React App uses REACT_APP_ prefix instead. Next.js uses NEXT_PUBLIC_.

Step 6 — Handle JWT Authentication

If your Spring Boot API uses JWT:

Add token to all requests:

// In your Axios client
client.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Handle 401 Unauthorized globally:

client.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem("token");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

Login flow:

export const authApi = {
  login: async (email, password) => {
    const res = await client.post("/api/auth/login", { email, password });
    localStorage.setItem("token", res.data.token);
    return res.data;
  },
  logout: () => {
    localStorage.removeItem("token");
  },
};

Step 7 — Production Setup

For production, you have two options:

Option A: Serve React from Spring Boot

Build React and copy the output to Spring Boot's static folder:

# In React project
npm run build
# Copy dist/ to src/main/resources/static/

Spring Boot serves static/ automatically. Your API and React app are on the same origin — no CORS needed.

Option B: Separate deployment with Nginx

Run React and Spring Boot on separate servers. Configure Nginx to proxy /api to Spring Boot:

location /api {
    proxy_pass http://localhost:8080;
}
location / {
    root /var/www/react-app;
    try_files $uri /index.html;
}

This is the more scalable approach for production applications.

Common Errors and Fixes

Error Cause Fix
CORS error in console Spring Boot not allowing the origin Add @CrossOrigin or CorsConfig
404 on API call Wrong URL in Axios Check Spring Boot is running on 8080, URL matches
401 Unauthorized JWT missing or expired Check Authorization header is being sent
Network Error Spring Boot not running Start the Spring Boot app first
Empty response Spring Boot returns null Check the service method returns data, not void

This is the exact integration pattern our students implement in CodeBegun's program — React frontend calling a Spring Boot API, with JWT auth, deployed to a Linux server. View the full curriculum → or WhatsApp us to join the next batch.

Share:LinkedInFacebookWhatsApp
Siva Prasad Galaba
Staff Engineer at Crunchyroll | Founder, CodeBegun

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaching the next generation to code the way the industry actually works.

← Back to all posts
Chat with us