Introduction to MERN Stack Architecture
The MERN stack is a powerful combination of technologies that enables developers to build robust, full-stack JavaScript applications:
- MongoDB: A NoSQL database that stores data in flexible, JSON-like documents
- Express.js: A minimal and flexible Node.js web application framework
- React: A JavaScript library for building user interfaces
- Node.js: A JavaScript runtime environment that executes JavaScript code outside a web browser
One of the key challenges in building a MERN application is establishing a robust connection between the React frontend and the Express backend. This lecture will focus on strategies for creating this connection effectively and securely.
Development Environment Setup
Project Structure Options
There are two common approaches to organizing a MERN stack project:
1. Separate Repositories/Projects
- frontend/ (React project)
- package.json
- public/
- src/
- components/
- pages/
- services/
- App.js
- index.js
- backend/ (Express project)
- package.json
- server.js
- routes/
- controllers/
- models/
- middleware/
2. Monorepo Approach
- mern-project/
- package.json
- client/
- package.json
- public/
- src/
- components/
- pages/
- services/
- App.js
- index.js
- server/
- package.json
- server.js
- routes/
- controllers/
- models/
- middleware/
Real-world analogy: Think of the monorepo approach like having multiple departments within a single office building, while the separate repositories approach is like having different branch offices that communicate with each other.
Setting Up a Monorepo Project
Let's walk through setting up a monorepo for a MERN stack application:
# Create project directory
mkdir my-mern-app
cd my-mern-app
# Initialize package.json for the root project
npm init -y
# Create client directory and initialize React app
npx create-react-app client
# Create server directory and initialize Express app
mkdir server
cd server
npm init -y
npm install express mongoose cors dotenv
# Create basic server files
touch server.js
mkdir routes controllers models middleware
Edit the root package.json to add scripts for running both client and server:
{
"name": "my-mern-app",
"version": "1.0.0",
"description": "A MERN stack application",
"main": "index.js",
"scripts": {
"start": "node server/server.js",
"server": "nodemon server/server.js",
"client": "npm start --prefix client",
"dev": "concurrently \"npm run server\" \"npm run client\"",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"concurrently": "^7.0.0",
"nodemon": "^2.0.15"
}
}
Install the dev dependencies:
npm install --save-dev concurrently nodemon
Setting Up the Express Backend
Let's create a basic Express server with an API endpoint that our React frontend can communicate with.
Basic Express Server (server.js)
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();
// Create Express app
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/api', (req, res) => {
res.json({ message: 'Welcome to the MERN API' });
});
// Include route files
const usersRoutes = require('./routes/users');
app.use('/api/users', usersRoutes);
// Connect to MongoDB
mongoose
.connect(process.env.MONGODB_URI)
.then(() => {
console.log('Connected to MongoDB');
// Start server after successful database connection
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch((err) => {
console.error('MongoDB connection error:', err);
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Something went wrong!' });
});
Creating a .env File
# server/.env
PORT=5000
MONGODB_URI=mongodb://localhost:27017/mern-app
JWT_SECRET=your_jwt_secret_key
Remember to add .env to your .gitignore file to prevent sensitive data from being committed to your repository.
Creating a Simple User Model (models/User.js)
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
password: {
type: String,
required: true,
minlength: 6,
},
role: {
type: String,
default: 'user',
enum: ['user', 'admin'],
},
},
{
timestamps: true,
}
);
// Hash password before saving
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) {
return next();
}
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Method to check password
userSchema.methods.comparePassword = async function (candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
const User = mongoose.model('User', userSchema);
module.exports = User;
Setting Up User Routes (routes/users.js)
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const jwt = require('jsonwebtoken');
// Get all users (admin only route in a real app)
router.get('/', async (req, res) => {
try {
const users = await User.find().select('-password');
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Register a new user
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Create new user
const user = new User({
name,
email,
password,
});
await user.save();
// Generate JWT
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '1d' }
);
res.status(201).json({
_id: user._id,
name: user.name,
email: user.email,
role: user.role,
token,
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Login user
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate JWT
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '1d' }
);
res.json({
_id: user._id,
name: user.name,
email: user.email,
role: user.role,
token,
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
module.exports = router;
Creating Authentication Middleware (middleware/auth.js)
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
// Get token from header
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'No authentication token, access denied' });
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Find user
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return res.status(401).json({ message: 'Token is valid, but user not found' });
}
// Attach user to request object
req.user = user;
next();
} catch (err) {
res.status(401).json({ message: 'Token is not valid' });
}
};
module.exports = auth;
Protected Route Example
// In routes/users.js, add:
const auth = require('../middleware/auth');
// Get user profile (protected route)
router.get('/profile', auth, async (req, res) => {
// req.user is available from the auth middleware
res.json(req.user);
});
Setting Up the React Frontend for API Communication
Now that we have our Express backend, let's configure our React frontend to communicate with it.
Configuring the Proxy
For development, we can use the proxy feature in React's package.json to forward API requests to our Express server without having to specify the full URL each time.
// client/package.json
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
// ...dependencies
},
"scripts": {
// ...scripts
},
"proxy": "http://localhost:5000"
}
Note: With this proxy setting, when you make a request to /api/users from your React app,
it will be forwarded to http://localhost:5000/api/users during development.
API Service with Axios
Let's create a service to handle API requests using Axios, a popular HTTP client library.
// client/src/services/api.js
import axios from 'axios';
// Create an axios instance
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding the auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for handling errors
api.interceptors.response.use(
(response) => response,
(error) => {
// Handle 401 - Unauthorized errors (token expired, etc.)
if (error.response && error.response.status === 401) {
localStorage.removeItem('token');
// Redirect to login page or refresh the token
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
Creating API Service Functions
// client/src/services/userService.js
import api from './api';
export const userService = {
register: async (userData) => {
const response = await api.post('/users/register', userData);
return response.data;
},
login: async (credentials) => {
const response = await api.post('/users/login', credentials);
// Store token in localStorage
if (response.data.token) {
localStorage.setItem('token', response.data.token);
}
return response.data;
},
logout: () => {
localStorage.removeItem('token');
},
getCurrentUser: async () => {
const response = await api.get('/users/profile');
return response.data;
},
getAllUsers: async () => {
const response = await api.get('/users');
return response.data;
},
};
Building React Components to Interact with API
Registration Form Component
// client/src/components/auth/Register.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { userService } from '../../services/userService';
function Register() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { name, email, password, confirmPassword } = formData;
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validation
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
// Clear previous errors
setError('');
setLoading(true);
try {
// Call the register service function
const userData = await userService.register({
name,
email,
password,
});
// Registration successful
console.log('User registered:', userData);
// Redirect to dashboard
navigate('/dashboard');
} catch (err) {
setError(
err.response?.data?.message ||
'An error occurred during registration'
);
} finally {
setLoading(false);
}
};
return (
<div className="auth-form-container">
<h2>Register</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={handleChange}
required
minLength="6"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={confirmPassword}
onChange={handleChange}
required
minLength="6"
/>
</div>
<button
type="submit"
className="auth-button"
disabled={loading}
>
{loading ? 'Registering...' : 'Register'}
</button>
</form>
<p>
Already have an account? <a href="/login">Login</a>
</p>
</div>
);
}
export default Register;
Login Form Component
// client/src/components/auth/Login.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { userService } from '../../services/userService';
function Login() {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { email, password } = formData;
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
// Clear previous errors
setError('');
setLoading(true);
try {
// Call the login service function
const userData = await userService.login({
email,
password,
});
// Login successful
console.log('User logged in:', userData);
// Redirect to dashboard
navigate('/dashboard');
} catch (err) {
setError(
err.response?.data?.message ||
'Invalid email or password'
);
} finally {
setLoading(false);
}
};
return (
<div className="auth-form-container">
<h2>Login</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={handleChange}
required
/>
</div>
<button
type="submit"
className="auth-button"
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<p>
Don't have an account? <a href="/register">Register</a>
</p>
</div>
);
}
export default Login;
Protected Profile Component
// client/src/components/profile/Profile.js
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { userService } from '../../services/userService';
function Profile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const navigate = useNavigate();
useEffect(() => {
// Fetch user profile
const fetchProfile = async () => {
try {
const userData = await userService.getCurrentUser();
setUser(userData);
} catch (err) {
console.error('Error fetching profile:', err);
setError('Failed to load profile. Please login again.');
// Redirect to login if authentication error
if (err.response && err.response.status === 401) {
navigate('/login');
}
} finally {
setLoading(false);
}
};
fetchProfile();
}, [navigate]);
const handleLogout = () => {
userService.logout();
navigate('/login');
};
if (loading) {
return <div>Loading profile...</div>;
}
if (error) {
return <div className="error-message">{error}</div>;
}
return (
<div className="profile-container">
<h2>User Profile</h2>
{user && (
<div className="profile-details">
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Role:</strong> {user.role}</p>
<p><strong>Joined:</strong> {new Date(user.createdAt).toLocaleDateString()}</p>
</div>
)}
<button
className="logout-button"
onClick={handleLogout}
>
Logout
</button>
</div>
);
}
export default Profile;
Authentication Context for Global State Management
To manage authentication state across the entire application, we can create a context provider.
// client/src/context/AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
import { userService } from '../services/userService';
// Create context
const AuthContext = createContext();
// Create provider component
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Check if user is logged in on initial load
useEffect(() => {
const loadUser = async () => {
try {
// Check if token exists
const token = localStorage.getItem('token');
if (!token) {
setLoading(false);
return;
}
// Token exists, try to get user data
const userData = await userService.getCurrentUser();
setUser(userData);
} catch (err) {
console.error('Authentication error:', err);
// Clear any invalid tokens
userService.logout();
} finally {
setLoading(false);
}
};
loadUser();
}, []);
// Login function
const login = async (credentials) => {
setLoading(true);
setError(null);
try {
const userData = await userService.login(credentials);
setUser(userData);
return userData;
} catch (err) {
setError(err.response?.data?.message || 'Login failed');
throw err;
} finally {
setLoading(false);
}
};
// Register function
const register = async (userData) => {
setLoading(true);
setError(null);
try {
const newUser = await userService.register(userData);
setUser(newUser);
return newUser;
} catch (err) {
setError(err.response?.data?.message || 'Registration failed');
throw err;
} finally {
setLoading(false);
}
};
// Logout function
const logout = () => {
userService.logout();
setUser(null);
};
return (
<AuthContext.Provider
value={{
user,
loading,
error,
login,
register,
logout,
isAuthenticated: !!user,
}}
>
{children}
</AuthContext.Provider>
);
}
// Custom hook to use auth context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Using the AuthContext in App.js
// client/src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
// Components
import Login from './components/auth/Login';
import Register from './components/auth/Register';
import Profile from './components/profile/Profile';
import Dashboard from './components/dashboard/Dashboard';
import NotFound from './components/layout/NotFound';
import Navbar from './components/layout/Navbar';
// Protected route component
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
};
function App() {
return (
<AuthProvider>
<Router>
<div className="app">
<Navbar />
<main className="container">
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
</Router>
</AuthProvider>
);
}
export default App;
Updated Login Component Using Context
// client/src/components/auth/Login.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
function Login() {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const { login, error, loading } = useAuth();
const navigate = useNavigate();
const { email, password } = formData;
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login({ email, password });
navigate('/dashboard');
} catch (err) {
// Error is handled by the auth context
console.error('Login error:', err);
}
};
return (
<div className="auth-form-container">
<h2>Login</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={handleChange}
required
/>
</div>
<button
type="submit"
className="auth-button"
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<p>
Don't have an account? <a href="/register">Register</a>
</p>
</div>
);
}
export default Login;
Handling CORS Issues
Cross-Origin Resource Sharing (CORS) is a common challenge when connecting frontend and backend services. CORS is a security feature implemented by browsers that restricts web pages from making requests to a different domain.
CORS Configuration on Express Server
// server/server.js
const cors = require('cors');
// Basic CORS setup
app.use(cors());
// Or with specific options
app.use(cors({
origin: 'http://localhost:3000', // Allow only your frontend domain
methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allowed HTTP methods
allowedHeaders: ['Content-Type', 'Authorization'], // Allowed headers
credentials: true, // Allow cookies to be sent
maxAge: 86400, // How long the results of a preflight request can be cached
}));
Real-world analogy: CORS is like a bouncer at an exclusive club. The bouncer checks if visitors (web requests) are on the guest list (allowed origins) before letting them in. If they're not on the list, they get turned away.
Common CORS Errors and Solutions
| Error | Solution |
|---|---|
| Access to fetch at 'http://localhost:5000/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy |
- Ensure cors middleware is properly configured - Check that the origin is allowed - Verify that all required headers are included in allowedHeaders |
| Preflight response is not successful |
- Make sure your server responds to OPTIONS requests - Check if the requested HTTP method is allowed - Ensure all required headers are allowed |
| The 'Access-Control-Allow-Origin' header has a value that is not equal to the supplied origin |
- Update your origin configuration to include the frontend domain - Consider using a wildcard (*) for development (not recommended for production) |
Environment Configuration for Different Environments
Different environments (development, testing, production) require different configurations. Let's set up our application to handle these environments correctly.
Server-side Environment Configuration
// server/.env.development
PORT=5000
MONGODB_URI=mongodb://localhost:27017/mern-dev
JWT_SECRET=dev_jwt_secret
NODE_ENV=development
// server/.env.production
PORT=8080
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/production-db
JWT_SECRET=production_jwt_secret_very_secure_and_long
NODE_ENV=production
Loading environment variables based on the environment:
// server/config/config.js
const dotenv = require('dotenv');
const path = require('path');
// Load environment variables based on NODE_ENV
const env = process.env.NODE_ENV || 'development';
const envPath = path.resolve(__dirname, `../.env.${env}`);
dotenv.config({ path: envPath });
module.exports = {
port: process.env.PORT || 5000,
mongoUri: process.env.MONGODB_URI,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV,
corsOrigin: process.env.CORS_ORIGIN || '*',
};
React Environment Variables
In React, we can use environment variables for configuration. Create these files in the client directory:
// client/.env.development
REACT_APP_API_URL=http://localhost:5000/api
REACT_APP_ENV=development
// client/.env.production
REACT_APP_API_URL=/api
REACT_APP_ENV=production
Note: In React, all environment variables must start with REACT_APP_ to be accessible in the application.
Using environment variables in React:
// client/src/services/api.js
import axios from 'axios';
// Get the API URL from environment variables
const apiUrl = process.env.REACT_APP_API_URL || '/api';
// Create an axios instance
const api = axios.create({
baseURL: apiUrl,
headers: {
'Content-Type': 'application/json',
},
});
// Rest of the api.js code...
export default api;
Deployment Considerations
When deploying a MERN application, there are several approaches to consider.
Option 1: Separate Deployment
- Deploy React frontend to a static hosting service (Netlify, Vercel, AWS S3)
- Deploy Express backend to a server hosting service (Heroku, DigitalOcean, AWS EC2)
- Configure CORS properly for cross-domain communication
Option 2: Single Server Deployment
// server/server.js - Add this code to serve React build files
const path = require('path');
// API routes
app.use('/api/users', usersRoutes);
// Other API routes...
// Serve static assets in production
if (process.env.NODE_ENV === 'production') {
// Set static folder
app.use(express.static(path.join(__dirname, '../client/build')));
// Serve the React frontend for any other route
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, '../client/build/index.html'));
});
}
Update package.json in the project root to include build scripts:
{
"scripts": {
"start": "node server/server.js",
"server": "nodemon server/server.js",
"client": "npm start --prefix client",
"dev": "concurrently \"npm run server\" \"npm run client\"",
"build": "npm run build --prefix client",
"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
}
}
Common Issues and Troubleshooting
CORS Errors
- Symptom: Console errors related to CORS policy
- Solution: Properly configure CORS middleware on the server
- Prevention: Always set up CORS early in the development process
Proxy Configuration Issues
- Symptom: API requests fail with 404 errors
- Solution: Verify proxy settings in package.json or setupProxy.js
- Prevention: Test API connectivity early in development
Authentication Token Problems
- Symptom: Authentication fails after working previously
- Solution: Check token expiration, storage, and proper inclusion in headers
- Prevention: Implement token refresh mechanism and proper error handling
Environment Variable Issues
- Symptom: Configuration doesn't apply or "undefined" values
- Solution: Ensure environment files are named correctly and variables are properly accessed
- Prevention: Use a config service that validates required environment variables
Practice Activities
Activity 1: Basic Connection Setup
Create a minimal MERN application with:
- Express server with a simple "ping" endpoint that returns a response
- React frontend that calls the ping endpoint and displays the response
- Proper error handling for network issues
Activity 2: Todo List API
Build a todo list application with:
- Express API with endpoints for CRUD operations on todos
- MongoDB integration to store todos
- React frontend to display, add, edit, and delete todos
Activity 3: Protected Routes
Extend Activity 2 to include:
- User authentication with JWT
- Protected routes on both frontend and backend
- User-specific todos (each user can only see their own todos)
Activity 4: Deployment Challenge
Deploy your MERN application using:
- Heroku for full-stack deployment
- MongoDB Atlas for the database
- Environment variables for configuration
Summary
- MERN Stack Structure: Understand how MongoDB, Express, React, and Node.js work together.
- Project Organization: Choose between separate repositories or a monorepo approach based on project needs.
- API Communication: Set up React to communicate with Express APIs using Axios and proper error handling.
- Authentication: Implement JWT-based authentication with proper token storage and protected routes.
- CORS Handling: Configure CORS properly to allow communication between frontend and backend.
- Environment Configuration: Set up different configurations for development and production environments.
- Deployment Strategies: Choose between separate or unified deployment approaches based on project requirements.
Connecting a React frontend to an Express backend is a fundamental skill for full-stack JavaScript development. The MERN stack offers a cohesive development experience with JavaScript throughout the entire application, making it easier to build modern web applications efficiently.