Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services

Blog Posts

Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Back to all blogs

How to Connect a React Front End to a Node.js Back End

Jun 12, 2026•8 min read

Your React front end and your Node.js back end are two separate programs. They don't share memory, variables, or function calls. They communicate the only way two independent programs on a network can: by sending HTTP requests and responses over the wire. Once that clicks, "connecting" React to Node stops being mysterious — you're just making the browser call a URL and rendering what comes back. I've wired this exact setup on every client project I've shipped, and the confusion almost always comes from expecting the two to be more connected than they are.

This is a deep dive under my full-stack app guide. Here I'm focused on one thing: getting the browser and the server to talk cleanly, in development and in production.

The client-server model

The mental model is a conversation:

text
1 React (browser, :5173) Node/Express (server, :4000) 2 │ │ 3 │ GET /api/tasks │ 4 │ ─────────────────────────────────────▶│ 5 │ │ query DB, build JSON 6 │ 200 OK [{...}, {...}] │ 7 │ ◀─────────────────────────────────────│ 8 │ render list │

The client (React) asks. The server (Node) answers. Neither knows anything about the other's internals — the server doesn't know React exists, and React doesn't know whether the server is Node, Python, or Go. All they share is the contract: URLs, methods, and JSON. That decoupling is a feature. You can rewrite either side independently as long as the HTTP contract holds.

The Node/Express API

Here's a minimal but complete API. Two pieces of middleware do the heavy lifting:

js
1// backend/server.js 2import express from 'express' 3import cors from 'cors' 4 5const app = express() 6 7app.use(cors({ origin: 'http://localhost:5173', credentials: true })) 8app.use(express.json()) 9 10app.get('/api/tasks', (req, res) => { 11 res.json([{ id: 1, title: 'Ship it' }]) 12}) 13 14app.post('/api/tasks', (req, res) => { 15 const { title } = req.body // populated by express.json() 16 res.status(201).json({ id: 2, title }) 17}) 18 19app.listen(4000, () => console.log('API on http://localhost:4000'))

express.json() parses incoming JSON request bodies into req.body — without it, req.body is undefined and every POST looks empty. cors(...) sets the response headers that tell the browser your API is willing to talk to the React origin. More on that next, because it's the single most common thing that trips people up.

The React side

React calls the API with fetch (built in) or axios (a small library some prefer). Both do the same job. Here's fetch:

jsx
1import { useEffect, useState } from 'react' 2 3export function App() { 4 const [tasks, setTasks] = useState([]) 5 6 useEffect(() => { 7 fetch('/api/tasks', { credentials: 'include' }) 8 .then((res) => { 9 if (!res.ok) throw new Error(`HTTP ${res.status}`) 10 return res.json() 11 }) 12 .then(setTasks) 13 .catch(console.error) 14 }, []) 15 16 async function addTask(title) { 17 const res = await fetch('/api/tasks', { 18 method: 'POST', 19 headers: { 'Content-Type': 'application/json' }, 20 credentials: 'include', 21 body: JSON.stringify({ title }), 22 }) 23 const task = await res.json() 24 setTasks((prev) => [...prev, task]) 25 } 26 27 return <button onClick={() => addTask('New')}>Add ({tasks.length})</button> 28}

The same with axios, if you'd rather:

js
1import axios from 'axios' 2const api = axios.create({ baseURL: '/api', withCredentials: true }) 3 4const { data } = await api.get('/tasks') 5await api.post('/tasks', { title: 'New' })

Axios sets Content-Type and parses JSON for you, and throws on non-2xx responses automatically — with fetch you check res.ok yourself. For most apps it's preference. I use fetch for small things and axios when I want interceptors (e.g. attaching an auth token to every request).

Why CORS exists

CORS — Cross-Origin Resource Sharing — is the thing everyone hits and few understand. Browsers enforce a same-origin policy: by default, JavaScript on localhost:5173 is not allowed to read responses from localhost:4000, because a different port means a different origin. This is a security boundary that stops a malicious site from quietly reading responses from other sites you're logged into.

Your API opts specific origins back in by sending Access-Control-Allow-Origin headers — which is exactly what the cors() middleware does. Two things to know:

  • CORS is enforced by the browser, not the server. curl and Postman ignore it entirely, which is why an endpoint can work in Postman but fail in the browser.
  • CORS is not a security feature for your API. It controls which web origins a browser will let read your responses; it does nothing against a direct request from a script or server. Real protection is server-side auth — a point I hammer in how to secure an API.

The dev proxy: skip CORS in development

Rather than fight CORS locally, I point the dev server at the API. Vite forwards any request starting with /api to localhost:4000, so from the browser's perspective everything is same-origin:

js
1// frontend/vite.config.js 2export default { 3 server: { 4 proxy: { 5 '/api': { target: 'http://localhost:4000', changeOrigin: true }, 6 }, 7 }, 8}

Create React App uses a one-line "proxy": "http://localhost:4000" in package.json instead. Either way, your React code calls /api/tasks with no host — which is also exactly how it'll behave in production if you serve both from the same origin. Relative URLs everywhere means nothing to change at deploy time.

Project structure

I keep the two apps in sibling folders. Clear boundary, independent dependencies:

text
1my-app/ 2├── backend/ 3│ ├── package.json 4│ ├── server.js 5│ └── routes/ 6│ └── tasks.js 7└── frontend/ 8 ├── package.json 9 ├── vite.config.js 10 └── src/ 11 ├── App.jsx 12 └── main.jsx

Each folder has its own package.json and node_modules because they're genuinely separate apps with separate dependencies — Express in one, React in the other. Some teams use a monorepo tool to manage both, but two plain folders is perfectly fine and easier to reason about.

Running both at once

In development you run two processes. Simplest is two terminals:

bash
1# terminal 1 2cd backend && node server.js 3 4# terminal 2 5cd frontend && npm run dev

To run them with one command, concurrently is the standard tool:

bash
1npm install -D concurrently
json
1{ 2 "scripts": { 3 "dev": "concurrently \"npm --prefix backend start\" \"npm --prefix frontend run dev\"" 4 } 5}

Production: two ways to ship

In production, React isn't served by the Vite dev server — you build it to static files (npm run build → a dist/ folder of HTML/CSS/JS). Then you have a choice:

Option 1 — serve the React build from Express (single origin). Point Express at the built files. One server, one domain, no CORS at all:

js
1import path from 'path' 2app.use(express.static(path.join(process.cwd(), '../frontend/dist'))) 3// SPA fallback: send index.html for any non-API route 4app.get(/^(?!\/api).*/, (req, res) => 5 res.sendFile(path.join(process.cwd(), '../frontend/dist/index.html')) 6)

Option 2 — separate hosts. Static files on a CDN or static host, the API on its own server, on different domains (app.example.com and api.example.com). More scalable, but now CORS is real — configure cors() with your production frontend origin.

I lean toward Option 2 for anything that needs to scale, because a CDN serves static assets far better than Node can, and the two tiers scale independently. Which one fits your app — and the deploy mechanics for both — is the subject of how to deploy a full-stack app.

Where to go next

You can now make React and Node talk in dev and prod. Two natural next steps: build out real data operations on this connection with how to build a CRUD application, and add sign-in so those requests are scoped to a user with how to authenticate users in a web application — where you'll see why credentials: 'include' in the fetch calls above matters. For the whole picture, head back to the full-stack app guide.

Related Posts

How to Build a Full-Stack App: A Step-by-Step Guide

The exact path I follow to ship a full-stack app end to end — data model, Node/Express API, database, React frontend, auth, security, and deploy — with links to the deep dives for each step.

How to Authenticate Users in a Web Application

Authentication has four parts: verifying identity, managing sessions, authorizing actions, and expiring access safely. I cover password hashing, sessions vs JWT, HttpOnly cookies, OAuth, passkeys, and the architecture I actually recommend.

How to Secure an API: A Practical Checklist

A layered, defense-in-depth checklist for securing an API — HTTPS, auth, RBAC and object-level checks against BOLA/IDOR, rate limiting, input validation, safe errors, logging, secret management, and the OWASP API Security Top 10. Plus the myths that get people breached.