Why Run React and Express Concurrently
Working with a React frontend and Express backend means managing two separate development servers. Traditionally, this requires multiple terminal windows--one for your React hot-reloading development server and another for your Express API. This setup creates friction in your development workflow, making it harder to iterate quickly and monitor both systems effectively.
The concurrently tool solves this problem by allowing you to run multiple command-line processes simultaneously from a single terminal. With proper configuration, you can start both your React and Express servers with one command, reducing cognitive overhead and speeding up your development cycle.
Managing multiple terminal tabs introduces context-switching overhead that interrupts your flow state. Each time you need to check backend logs, you must shift focus to a different window. When making rapid iterations between frontend changes and API adjustments, the constant tab-switching breaks concentration and slows momentum. By running both servers concurrently in a unified view, you maintain focus on the task at hand while keeping visibility into both systems.
This approach is essential for modern full-stack web development workflows where JavaScript powers both the user interface and server-side logic. The ability to iterate quickly between frontend and backend changes significantly improves development velocity and code quality.
According to developer workflow studies, reducing context switches significantly improves productivity in coding environments where multiple processes need monitoring simultaneously. The mental load of remembering which tab controls which service adds unnecessary complexity to already challenging full-stack development tasks.
Development Efficiency Gains
50%
Reduced terminal switching
1
Command to start everything
2
Servers running in parallel
Project Structure Setup
Before configuring concurrently, you need a well-organized project structure that separates your React frontend from your Express backend while keeping them in a unified repository. This separation allows each application to maintain its own dependencies and configuration while enabling easy communication between them.
The recommended approach uses a root-level directory containing two main folders: client for your React application and server for your Express API. This structure keeps concerns separate while making it easy to coordinate development workflows.
Recommended Folder Structure
my-fullstack-app/
├── client/ # React application
│ ├── public/
│ ├── src/
│ ├── package.json
│ └── .env
├── server/ # Express application
│ ├── routes/
│ ├── models/
│ ├── index.js
│ ├── package.json
│ └── .env
├── package.json # Root package.json for concurrently
└── .gitignore
Creating the Project Structure
Initialize your project with these terminal commands:
# Create root directory and initialize npm
mkdir my-fullstack-app
cd my-fullstack-app
npm init -y
# Create React app using Vite (faster than Create React App)
npm create vite@latest client -- --template react
cd client
npm install
cd ..
# Create Express server
mkdir server
cd server
npm init -y
npm install express cors dotenv
npm install --save-dev nodemon
cd ..
This structure follows the LogRocket approach for organizing full-stack JavaScript projects with clear separation between frontend and backend concerns. Each application owns its dependencies, preventing version conflicts between React and Node.js packages.
For teams building modern web applications, this monorepo-style structure simplifies dependency management and enables consistent development practices across the entire stack.
Installing and Configuring Concurrently
The concurrently tool is a npm package that enables running multiple commands simultaneously. It handles process management, output formatting, and termination coordination between different services. Installing it as a development dependency ensures your production deployments remain unaffected.
Installation
Install concurrently at your project root:
npm install concurrently --save-dev
Package.json Configuration
Configure your root package.json to coordinate both services. The scripts section defines how concurrently executes your frontend and backend servers.
{
"name": "my-fullstack-app",
"version": "1.0.0",
"scripts": {
"client": "npm run start --prefix client",
"server": "npm run dev --prefix server",
"dev": "concurrently \"npm run server\" \"npm run client\""
},
"devDependencies": {
"concurrently": "^9.0.0"
}
}
The --prefix flag tells npm to execute the command in the specified directory, allowing you to target the client and server folders independently. When you run npm run dev, concurrently starts both processes and displays their output in a unified terminal view.
Advanced Script Variations
For different development scenarios, you can extend your package.json with additional scripts:
{
"scripts": {
"client": "npm run start --prefix client",
"server": "npm run dev --prefix server",
"dev": "concurrently \"npm run server\" \"npm run client\"",
"dev:kill": "concurrently \"npm run server\" \"npm run client\" --kill-others",
"dev:raw": "concurrently \"npm run server\" \"npm run client\" --raw",
"build:all": "npm run build --prefix client && echo 'Client built successfully'",
"test:all": "concurrently \"npm test --prefix client\" \"npm test --prefix server\""
}
}
The --kill-others flag ensures that if one process fails, all other processes terminate automatically, preventing orphaned server processes. The --raw flag outputs raw terminal output without color formatting, which is useful when integrating with CI/CD systems or log aggregation tools. These options provide flexibility for different debugging and deployment workflows.
Integrating tools like concurrently is a best practice in professional web development workflows, enabling teams to maintain high productivity across complex full-stack applications.
Express Backend Setup
Your Express backend handles API requests from your React frontend. Proper setup includes initializing the application, configuring middleware, and creating routes that your frontend can consume. The server typically runs on a different port than React (commonly 3001, 5000, or 8080) to avoid conflicts.
Basic Express Server
// server/index.js
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
app.use(express.json());
// Routes
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
app.get('/api/data', (req, res) => {
res.json({ message: 'Data from Express server' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Server package.json Scripts
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"express": "^4.18.0",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
Understanding Express Middleware
Each middleware serves a specific purpose in your API pipeline. The cors middleware enables Cross-Origin Resource Sharing, allowing your React app on a different port to communicate with this server. Without it, browsers block requests between different origins for security reasons.
The express.json() middleware parses incoming JSON payloads in request bodies, making the data accessible via req.body. This is essential for POST and PUT requests that send data to your API. Additional middleware like helmet adds security headers, morgan provides request logging, and express-rate-limit prevents abuse.
Error handling middleware should come last in your pipeline to catch exceptions from all preceding routes:
// Global error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
For database integration, install appropriate drivers like mongoose for MongoDB or pg for PostgreSQL, then establish connections in your server's initialization code using environment variables for configuration.
Building robust Express backends is a core component of full-stack JavaScript development, enabling secure and scalable API endpoints for your web applications.
CORS Configuration Options
The cors middleware provides several configuration options beyond simple origin matching:
const cors = require('cors');
// Allow specific origin with credentials
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
// Allow multiple origins
app.use(cors({
origin: ['http://localhost:3000', 'http://localhost:3001'],
credentials: true
}));
// Dynamic origin based on request
app.use(cors({
origin: function(origin, callback) {
// Allow requests with no origin (mobile apps, curl requests)
if(!origin) return callback(null, true);
if(['http://localhost:3000'].includes(origin)) {
return callback(null, true);
}
callback(new Error('Not allowed by CORS'));
}
}));
// Production-ready configuration
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Security Best Practices for CORS
In production environments, never use wildcard (*) origins for APIs that handle sensitive data. Instead, whitelist specific domains through environment variables. The credentials: true option requires your frontend to include withCredentials headers, which has security implications for cookie-based authentication.
Restrict allowed HTTP methods and headers to the minimum required for your API. This reduces the attack surface if your API is accidentally exposed to unauthorized origins. Logging blocked CORS requests helps identify misconfigured clients or potential security issues.
Always validate origin patterns against a known whitelist rather than using string matching that could be bypassed. For microservices architectures, consider implementing a dedicated CORS proxy or API gateway that centralizes origin validation across all your services.
Proper CORS configuration is critical for secure web application development, ensuring your APIs are accessible only to authorized clients while maintaining robust security boundaries.
React Frontend Integration
With your Express server configured, the next step is integrating API calls into your React application. Modern React development typically uses either the native fetch API or Axios for HTTP requests. Axios provides a more declarative API with automatic JSON transformation and error handling benefits.
Installing Axios
cd client
npm install axios
Creating an API Service
// client/src/services/api.js
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:5000',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor for auth tokens
export const fetchData = async () => {
try {
const response = await api.get('/api/data');
return response.data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
};
export default api;
React Component Integration
// client/src/components/DataDisplay.jsx
import React, { useState, useEffect } from 'react';
import { fetchData } from '../services/api';
const DataDisplay = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadData = async () => {
try {
const result = await fetchData();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>Data from Server</h2>
<p>{data.message}</p>
</div>
);
};
export default DataDisplay;
React Hooks Patterns for Data Fetching
The useEffect hook handles side effects like API calls, running after the component renders. The empty dependency array [] ensures the data loads only once when the component mounts. Combining useState with useEffect provides a clean pattern for handling loading states, successful data, and error conditions.
For more complex scenarios, consider custom hooks that encapsulate API logic:
// useApi hook for reusable data fetching
const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
Environment-specific API URLs are managed through .env.development and .env.production files, with React automatically loading variables prefixed with REACT_APP_. This keeps sensitive configuration out of your codebase while supporting multiple deployment environments.
Building seamless React applications with robust API integration requires understanding both frontend state management and backend communication patterns.
Environment Configuration
Managing environment variables across both React and Express applications ensures your development, staging, and production configurations remain separate. React uses the REACT_APP_ prefix for environment variables, while Express typically uses standard environment variables or a .env file with the dotenv package.
Client Environment Variables
# client/.env
REACT_APP_API_URL=http://localhost:5000
REACT_APP_ENV=development
Server Environment Variables
# server/.env
PORT=5000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
Root Coordination
You can also create a root .env file for concurrent-specific variables:
# .env
CLIENT_PORT=3000
SERVER_PORT=5000
This approach allows you to keep all configuration in one place and pass values to both client and server through npm scripts.
Security Best Practices for Environment Variables
Never commit .env files to version control. Add .env to your .gitignore file to prevent accidentally exposing secrets. Use different variable names for different environments--development, staging, and production should use separate credentials wherever possible.
For API keys and database credentials, rotate them immediately if exposed. Consider using a secrets management service like HashiCorp Vault or your hosting platform's secret management for production deployments. Frontend environment variables are embedded at build time and visible to anyone who inspects your JavaScript bundles, so never expose sensitive secrets in React environment variables.
Use descriptive variable names that indicate purpose and expected format. Document all required environment variables in a .env.example file (without values) so other developers know what configuration your application requires. This improves onboarding and reduces configuration errors.
Effective environment management is crucial for professional web development practices, enabling secure and flexible deployments across multiple environments.
Streamlined workflows for modern full-stack development
Single Command Start
Start both frontend and backend servers with one npm command, eliminating terminal tab switching.
Unified Output
View logs from both servers in one terminal window, making debugging more efficient.
Coordinated Shutdown
Ctrl+C terminates all running processes simultaneously, preventing orphaned servers.
Faster Iteration
Make changes to either frontend or backend and see results immediately without manual process management.
Production Considerations
While concurrently is designed for development workflows, deploying a React + Express application to production requires different strategies. You have several options depending on your hosting platform, scalability requirements, and operational preferences.
Build React for Production
First, build your React application into static files:
cd client
npm run build
This creates a build/ directory with optimized, minified JavaScript, CSS, and HTML files.
Express Serves Static Files
Configure Express to serve the React build in production:
// server/index.js (production setup)
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../client/build')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../client/build', 'index.html'));
});
}
Alternative Deployment Strategies
-
Separate Hosting: Deploy React to Vercel or Netlify for optimized static file serving and automatic SSL. Deploy Express to Heroku, Render, or Railway for managed Node.js hosting. This approach separates concerns and allows each service to scale independently.
-
Containerization: Use Docker to containerize both services behind a reverse proxy like Nginx. This provides consistent environments from development through production and simplifies horizontal scaling with container orchestration platforms.
-
Platform-Specific: Use integrated platforms like Vercel (with serverless functions) or Railway that handle both frontend and backend in a unified deployment. These platforms abstract infrastructure complexity but may have limitations for complex backend requirements.
The choice depends on your team's expertise, expected traffic patterns, and operational requirements. Smaller projects often benefit from separate hosting for simplicity, while larger applications may need containerization for consistent scaling and deployment pipelines.
Implementing production-ready web development solutions requires careful consideration of deployment architecture, scalability needs, and operational overhead.