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:
```java
@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:
```java
@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:
```bash
npm install axios
```
Create an Axios instance with your base URL:
**`src/api/client.js`**
```javascript
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`**
```javascript
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`**
```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)
```jsx
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:
```javascript
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:**
```javascript
// 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:**
```javascript
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:**
```javascript
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:
```bash
# 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:
```nginx
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 →](/java-full-stack) or [WhatsApp us](https://wa.me/916301099587) to join the next batch.
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.
