Integrating React with Express.js and Node.js

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:

  1. React calls an Express API (/api/...)
  2. Express reads request, talks to DB, returns JSON
  3. 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 Authorization header 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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *