The big picture
- React runs in the browser (frontend UI).
- Express + Node.js runs on a server (backend APIs).
- They communicate over HTTP using JSON (most common).
Typical flow:
- React calls an Express API (
/api/...) - Express reads request, talks to DB, returns JSON
- React updates UI based on response
1) Connecting React frontend with Express.js backend
Option A: Run both separately (common in dev)
- React dev server:
http://localhost:3000 - Express server:
http://localhost:5000
React sends requests to backend using full URL.
Backend (Express) basic setup
// server.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/health", (req, res) => {
res.json({ ok: true, message: "API is running" });
});
app.listen(5000, () => console.log("Server running on http://localhost:5000"));
Frontend (React) test call
fetch("http://localhost:5000/api/health")
.then(r => r.json())
.then(console.log);
Option B: Proxy requests in React (recommended for dev)
So you can call /api/... without hardcoding backend URL.
Create React App: add to package.json
{
"proxy": "http://localhost:5000"
}
Then in React:
fetch("/api/health")
Vite: add proxy in vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:5000",
},
},
});
Option C: Serve React build from Express (common in production)
- Build React → static files
- Express serves them
import path from "path";
import express from "express";
const app = express();
app.use(express.json());
app.use(express.static(path.join(process.cwd(), "client/dist"))); // Vite build
app.get("/api/health", (req, res) => res.json({ ok: true }));
app.get("*", (req, res) => {
res.sendFile(path.join(process.cwd(), "client/dist/index.html"));
});
app.listen(5000);
2) Handling CORS issues (Cross-Origin Resource Sharing)
Why CORS happens
If React runs on localhost:3000 and API runs on localhost:5000, that’s different origin, so the browser blocks requests unless backend allows it.
Explain with simple rule
- Browser enforces CORS (server must allow it)
- Fix is done on Express backend
Use cors middleware (recommended)
Install:
npm i cors
Use:
import cors from "cors";
app.use(cors({
origin: "http://localhost:3000", // or your deployed frontend URL
credentials: true
}));
If you only use token in headers (JWT in Authorization), you usually don’t need credentials: true.
Quick dev-only allow all
app.use(cors());
(Use stricter config for production.)
3) Making HTTP requests from React to Express APIs
Using fetch (built-in)
const res = await fetch("/api/users");
const data = await res.json();
Sending JSON (POST)
await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Amit" }),
});
Using axios (popular)
Install:
npm i axios
import axios from "axios";
const { data } = await axios.get("/api/users");
POST:
await axios.post("/api/users", { name: "Amit" });
4) Passing data between frontend and backend
Frontend → Backend (request)
You pass data via:
- URL params:
/api/users/123 - Query string:
/api/users?role=admin - Request body (POST/PUT)
- Headers (Authorization token)
Express example
app.post("/api/tasks", (req, res) => {
const { title, done } = req.body;
res.json({ created: true, task: { id: 1, title, done } });
});
Backend → Frontend (response)
Respond with JSON:
res.status(200).json({ ok: true, data: result });
React reads:
const data = await res.json();
setState(data);
Best practice response format
Use consistent response shape:
{ "success": true, "data": {...}, "error": null }
5) Authentication and authorization using JWT (JSON Web Tokens)
What JWT does
- User logs in with credentials
- Backend verifies and returns a token
- React stores token (usually memory or localStorage)
- React sends token in
Authorizationheader for protected routes - Backend verifies token on every request
Backend: Generate JWT on login
Install:
npm i jsonwebtoken bcrypt
Login route
import jwt from "jsonwebtoken";
app.post("/api/auth/login", async (req, res) => {
const { email, password } = req.body;
// 1) Validate user (example only)
if (email !== "test@example.com" || password !== "1234") {
return res.status(401).json({ success: false, error: "Invalid credentials" });
}
// 2) Create token payload
const payload = { userId: 1, role: "user" };
// 3) Sign token
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" });
res.json({ success: true, token });
});
Set .env:
JWT_SECRET=supersecretkey
Backend: Protect routes using JWT middleware
import jwt from "jsonwebtoken";
function auth(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith("Bearer ")) {
return res.status(401).json({ success: false, error: "Missing token" });
}
const token = header.split(" ")[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // { userId, role, iat, exp }
next();
} catch (err) {
return res.status(401).json({ success: false, error: "Invalid/Expired token" });
}
}
app.get("/api/profile", auth, (req, res) => {
res.json({ success: true, user: req.user });
});
Frontend: Store token and send it in requests
Login request
async function login(email, password) {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
localStorage.setItem("token", data.token);
}
Use token in protected API calls
async function getProfile() {
const token = localStorage.getItem("token");
const res = await fetch("/api/profile", {
headers: { Authorization: `Bearer ${token}` },
});
return res.json();
}
Authorization (roles/permissions)
Authentication = “who you are”
Authorization = “what you can do”
Example: Admin-only route
function requireRole(role) {
return (req, res, next) => {
if (req.user?.role !== role) {
return res.status(403).json({ success: false, error: "Forbidden" });
}
next();
};
}
app.delete("/api/admin/users/:id", auth, requireRole("admin"), (req, res) => {
res.json({ success: true });
});
Security best practices (important)
- Prefer HttpOnly cookies for JWT in production (prevents XSS token theft)
- If using localStorage, protect against XSS aggressively
- Use short token expiry + refresh tokens (advanced)
- Always hash passwords with bcrypt
- Use HTTPS in production
- Lock down CORS to only your frontend domain
Leave a Reply