Full-Stack Application
Learning Objectives
- By the end of this project, you will be able to:
- - Connect React frontend to Node.js backend
- - Implement authentication flow
- - Manage API calls from frontend
- - Handle authentication state
- - Build complete full-stack applications
- - Deploy full-stack applications
- - Handle CORS and security
Project 3.2: Full-Stack Application
Project Overview
Build a complete full-stack application combining the React frontend with the Node.js backend. This project will help you practice connecting frontend and backend, managing authentication across the stack, and building production-ready applications.
Learning Objectives
By the end of this project, you will be able to:
- Connect React frontend to Node.js backend
- Implement authentication flow
- Manage API calls from frontend
- Handle authentication state
- Build complete full-stack applications
- Deploy full-stack applications
- Handle CORS and security
Project Requirements
Core Features
- User Authentication: Register, login, logout
- Product Management: View, create, edit, delete products
- Protected Routes: Secure frontend routes
- API Integration: Connect frontend to backend
- State Management: Manage auth and data state
- Error Handling: Handle API errors
- Loading States: Show loading indicators
- Responsive Design: Works on all devices
Technical Requirements
- React frontend
- Node.js/Express backend
- MongoDB database
- JWT authentication
- Axios for API calls
- Context API for state
- Protected routes
Project Structure
fullstack-app/
├── backend/
│ ├── src/
│ │ ├── controllers/
│ │ ├── models/
│ │ ├── routes/
│ │ ├── middleware/
│ │ └── server.js
│ ├── .env
│ └── package.json
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── context/
│ │ ├── services/
│ │ └── App.jsx
│ └── package.json
└── README.md
Step-by-Step Implementation
Backend Setup
Use the backend from Project 3.1 or set it up following those instructions.
Frontend Setup
# Create React app
npx create-react-app frontend
cd frontend
# Install dependencies
npm install axios react-router-dom
# Start development server
npm start
Frontend API Service
// src/services/api.js
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
// Create axios instance
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Add token to requests
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Handle response errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Auth API
export const authAPI = {
register: (userData) => api.post('/auth/register', userData),
login: (credentials) => api.post('/auth/login', credentials),
getMe: () => api.get('/auth/me')
};
// Products API
export const productsAPI = {
getProducts: (params) => api.get('/products', { params }),
getProduct: (id) => api.get(`/products/${id}`),
createProduct: (productData) => api.post('/products', productData),
updateProduct: (id, productData) => api.put(`/products/${id}`, productData),
deleteProduct: (id) => api.delete(`/products/${id}`)
};
export default api;
Auth Context
// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { authAPI } from '../services/api';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [token, setToken] = useState(localStorage.getItem('token'));
useEffect(() => {
if (token) {
loadUser();
} else {
setLoading(false);
}
}, [token]);
const loadUser = async () => {
try {
const response = await authAPI.getMe();
setUser(response.data.user);
} catch (error) {
localStorage.removeItem('token');
setToken(null);
} finally {
setLoading(false);
}
};
const register = async (userData) => {
try {
const response = await authAPI.register(userData);
const { token, user } = response.data;
localStorage.setItem('token', token);
setToken(token);
setUser(user);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data?.error || 'Registration failed'
};
}
};
const login = async (credentials) => {
try {
const response = await authAPI.login(credentials);
const { token, user } = response.data;
localStorage.setItem('token', token);
setToken(token);
setUser(user);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data?.error || 'Login failed'
};
}
};
const logout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider
value={{
user,
loading,
register,
login,
logout,
isAuthenticated: !!user
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Protected Route Component
// src/components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
return isAuthenticated ? children : <Navigate to="/login" />;
}
export default ProtectedRoute;
Login Page
// src/pages/Login.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import './Login.css';
function Login() {
const navigate = useNavigate();
const { login } = useAuth();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
const result = await login(formData);
if (result.success) {
navigate('/');
} else {
setError(result.error);
}
setLoading(false);
};
return (
<div className="login-page">
<div className="login-container">
<h1>Login</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Email</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<button type="submit" disabled={loading} className="btn btn-primary">
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<p>
Don't have an account? <Link to="/register">Register</Link>
</p>
</div>
</div>
);
}
export default Login;
Register Page
// src/pages/Register.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import './Register.css';
function Register() {
const navigate = useNavigate();
const { register } = useAuth();
const [formData, setFormData] = useState({
name: '',
email: '',
password: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
const result = await register(formData);
if (result.success) {
navigate('/');
} else {
setError(result.error);
}
setLoading(false);
};
return (
<div className="register-page">
<div className="register-container">
<h1>Register</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Name</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label>Email</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
minLength="6"
/>
</div>
<button type="submit" disabled={loading} className="btn btn-primary">
{loading ? 'Registering...' : 'Register'}
</button>
</form>
<p>
Already have an account? <Link to="/login">Login</Link>
</p>
</div>
</div>
);
}
export default Register;
Products Page
// src/pages/Products.jsx
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { productsAPI } from '../services/api';
import { useAuth } from '../context/AuthContext';
import './Products.css';
function Products() {
const { isAuthenticated } = useAuth();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [filters, setFilters] = useState({
search: '',
category: ''
});
useEffect(() => {
loadProducts();
}, [filters]);
const loadProducts = async () => {
try {
setLoading(true);
const response = await productsAPI.getProducts(filters);
setProducts(response.data.data);
} catch (error) {
setError('Failed to load products');
} finally {
setLoading(false);
}
};
const handleDelete = async (id) => {
if (window.confirm('Are you sure you want to delete this product?')) {
try {
await productsAPI.deleteProduct(id);
loadProducts();
} catch (error) {
alert('Failed to delete product');
}
}
};
if (loading) {
return <div className="loading">Loading products...</div>;
}
return (
<div className="products-page">
<div className="products-header">
<h1>Products</h1>
{isAuthenticated && (
<Link to="/products/new" className="btn btn-primary">
Add Product
</Link>
)}
</div>
<div className="filters">
<input
type="text"
placeholder="Search products..."
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
/>
<select
value={filters.category}
onChange={(e) => setFilters({ ...filters, category: e.target.value })}
>
<option value="">All Categories</option>
<option value="Electronics">Electronics</option>
<option value="Accessories">Accessories</option>
</select>
</div>
{error && <div className="error">{error}</div>}
<div className="products-grid">
{products.map(product => (
<div key={product._id} className="product-card">
<Link to={`/products/${product._id}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price.toFixed(2)}</p>
</Link>
{isAuthenticated && (
<div className="product-actions">
<Link
to={`/products/${product._id}/edit`}
className="btn btn-edit"
>
Edit
</Link>
<button
onClick={() => handleDelete(product._id)}
className="btn btn-delete"
>
Delete
</button>
</div>
)}
</div>
))}
</div>
</div>
);
}
export default Products;
Create Product Page
// src/pages/CreateProduct.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { productsAPI } from '../services/api';
import './CreateProduct.css';
function CreateProduct() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
name: '',
description: '',
price: '',
category: '',
image: 'https://via.placeholder.com/300',
inStock: true,
stock: 0
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setFormData({
...formData,
[e.target.name]: value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await productsAPI.createProduct({
...formData,
price: parseFloat(formData.price),
stock: parseInt(formData.stock)
});
navigate('/products');
} catch (error) {
setError(error.response?.data?.error || 'Failed to create product');
} finally {
setLoading(false);
}
};
return (
<div className="create-product-page">
<h1>Create Product</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit} className="product-form">
<div className="form-group">
<label>Name</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
required
/>
</div>
<div className="form-row">
<div className="form-group">
<label>Price</label>
<input
type="number"
name="price"
value={formData.price}
onChange={handleChange}
step="0.01"
min="0"
required
/>
</div>
<div className="form-group">
<label>Category</label>
<select
name="category"
value={formData.category}
onChange={handleChange}
required
>
<option value="">Select category</option>
<option value="Electronics">Electronics</option>
<option value="Accessories">Accessories</option>
</select>
</div>
</div>
<div className="form-group">
<label>Image URL</label>
<input
type="url"
name="image"
value={formData.image}
onChange={handleChange}
/>
</div>
<div className="form-row">
<div className="form-group">
<label>Stock</label>
<input
type="number"
name="stock"
value={formData.stock}
onChange={handleChange}
min="0"
/>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="inStock"
checked={formData.inStock}
onChange={handleChange}
/>
In Stock
</label>
</div>
</div>
<div className="form-actions">
<button type="submit" disabled={loading} className="btn btn-primary">
{loading ? 'Creating...' : 'Create Product'}
</button>
<button
type="button"
onClick={() => navigate('/products')}
className="btn btn-secondary"
>
Cancel
</button>
</div>
</form>
</div>
);
}
export default CreateProduct;
App Component with Routes
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import Navigation from './components/Navigation';
import Home from './pages/Home';
import Login from './pages/Login';
import Register from './pages/Register';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import CreateProduct from './pages/CreateProduct';
import EditProduct from './pages/EditProduct';
import './App.css';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Navigation />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route
path="/products/new"
element={
<ProtectedRoute>
<CreateProduct />
</ProtectedRoute>
}
/>
<Route
path="/products/:id/edit"
element={
<ProtectedRoute>
<EditProduct />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
Navigation Component
// src/components/Navigation.jsx
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import './Navigation.css';
function Navigation() {
const { isAuthenticated, user, logout } = useAuth();
return (
<nav className="navigation">
<div className="nav-container">
<Link to="/" className="nav-logo">
E-Commerce
</Link>
<div className="nav-links">
<Link to="/" className="nav-link">Home</Link>
<Link to="/products" className="nav-link">Products</Link>
{isAuthenticated ? (
<>
<span className="nav-user">Welcome, {user?.name}</span>
<button onClick={logout} className="btn btn-logout">
Logout
</button>
</>
) : (
<>
<Link to="/login" className="nav-link">Login</Link>
<Link to="/register" className="nav-link">Register</Link>
</>
)}
</div>
</div>
</nav>
);
}
export default Navigation;
Features Implementation
Frontend-Backend Connection
- API Service: Centralized API calls
- Axios Interceptors: Automatic token handling
- Error Handling: Proper error responses
- Loading States: User feedback
Authentication Flow
- Register: Create account, get token
- Login: Authenticate, get token
- Token Storage: Save in localStorage
- Protected Routes: Require authentication
- Auto-logout: On token expiration
State Management
- Auth Context: Global auth state
- API State: Component-level state
- Synchronization: Keep frontend/backend in sync
Testing Your Application
Manual Testing Checklist
- [ ] Register new user
- [ ] Login with credentials
- [ ] View products
- [ ] Create product (authenticated)
- [ ] Edit product (authenticated)
- [ ] Delete product (authenticated)
- [ ] Logout
- [ ] Access protected routes
- [ ] Test error handling
Exercise: Full-Stack App
Instructions:
- Set up both frontend and backend
- Connect them together
- Test all features
- Deploy application
- Add enhancements
Enhancement Ideas:
- Add order management
- Add shopping cart
- Add user profile page
- Add product reviews
- Add image upload
- Add email notifications
- Add admin dashboard
- Add analytics
Deployment
Backend Deployment
# Deploy to Heroku, Railway, or similar
# Set environment variables
# Update CORS settings
Frontend Deployment
# Build for production
npm run build
# Deploy to Vercel, Netlify, or similar
# Set REACT_APP_API_URL environment variable
Common Issues and Solutions
Issue: CORS errors
Solution: Configure CORS in backend to allow frontend origin.
Issue: Token not sent
Solution: Check axios interceptors and localStorage.
Issue: API calls failing
Solution: Verify API URL and backend is running.
Quiz: Full-Stack Concepts
-
Full-stack app:
- A) Frontend + Backend
- B) Frontend only
- C) Both
- D) Neither
-
Axios:
- A) HTTP client
- B) Database
- C) Both
- D) Neither
-
Context API:
- A) Manages global state
- B) Doesn't manage state
- C) Both
- D) Neither
-
Protected routes:
- A) Require authentication
- B) Don't require authentication
- C) Both
- D) Neither
-
CORS:
- A) Cross-origin resource sharing
- B) Same-origin only
- C) Both
- D) Neither
Answers:
- A) Frontend + Backend
- A) HTTP client
- A) Manages global state
- A) Require authentication
- A) Cross-origin resource sharing
Key Takeaways
- Full-Stack: Combine frontend and backend
- API Integration: Connect React to Express
- Authentication: Manage auth across stack
- State Management: Sync frontend/backend
- Best Practice: Clean architecture, security
Next Steps
Congratulations! You've built a Full-Stack Application. You now know:
- How to connect frontend and backend
- How to implement authentication
- How to manage state
- How to build complete applications
What's Next?
- Project 4: Advanced Projects
- Learn WebSockets
- Build real-time applications
- Create advanced features
Project completed! You've finished Project 3: Full-Stack Applications. Ready for Project 4: Advanced Projects!
Course Navigation
- REST API with Node.js
- Full-Stack Application