🎯 Learning Objectives
After completing this course, you will be able to:
- Build complete web applications from scratch
- Integrate frontend and backend seamlessly
- Implement authentication and authorization
- Design and manage databases
- Deploy full stack applications
- Handle real-time features
- Follow full stack best practices
⚡ Prerequisites
- Frontend Development Course completed
- Backend Development Course completed
- REST APIs knowledge
- Database fundamentals
- Git/GitHub proficiency
1. Full Stack Architecture
The MERN Stack
We'll use the MERN stack (MongoDB, Express, React, Node.js) as our primary example, but the concepts apply to any stack.
Frontend Layer
- React: UI components
- React Router: Navigation
- Axios: HTTP client
- Context/Redux: State management
- Tailwind CSS: Styling
Backend Layer
- Node.js: Runtime
- Express: Web framework
- JWT: Authentication
- Mongoose: ODM
- Validation: Input validation
Database Layer
- MongoDB: NoSQL database
- Redis: Caching
- PostgreSQL: Relational option
DevOps Layer
- Docker: Containerization
- GitHub Actions: CI/CD
- AWS/Vercel: Hosting
- Nginx: Reverse proxy
Project Structure
fullstack-app/
├── client/ # Frontend (React)
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ │ ├── auth/
│ │ │ │ ├── Login.tsx
│ │ │ │ └── Register.tsx
│ │ │ ├── common/
│ │ │ │ ├── Header.tsx
│ │ │ │ └── Footer.tsx
│ │ │ └── dashboard/
│ │ ├── contexts/
│ │ │ └── AuthContext.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ ├── services/
│ │ │ └── api.ts
│ │ ├── types/
│ │ ├── utils/
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── package.json
│ └── vite.config.ts
│
├── server/ # Backend (Node.js/Express)
│ ├── src/
│ │ ├── config/
│ │ │ ├── database.js
│ │ │ └── env.js
│ │ ├── controllers/
│ │ │ ├── authController.js
│ │ │ └── userController.js
│ │ ├── middleware/
│ │ │ ├── auth.js
│ │ │ ├── errorHandler.js
│ │ │ └── validation.js
│ │ ├── models/
│ │ │ ├── User.js
│ │ │ └── Post.js
│ │ ├── routes/
│ │ │ ├── auth.js
│ │ │ └── users.js
│ │ ├── services/
│ │ │ └── emailService.js
│ │ ├── utils/
│ │ │ └── helpers.js
│ │ └── server.js
│ ├── package.json
│ └── .env
│
├── docker-compose.yml
├── .gitignore
└── README.md
2. Authentication & Authorization
Backend: JWT Authentication
// server/src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true,
minlength: 6
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
refreshTokens: [String],
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
// server/src/controllers/authController.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const generateAccessToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
};
const generateRefreshToken = (userId) => {
return jwt.sign(
{ userId },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
};
exports.register = async (req, res) => {
try {
const { username, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
message: 'User already exists'
});
}
// Create user
const user = new User({ username, email, password });
await user.save();
// Generate tokens
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Save refresh token
user.refreshTokens.push(refreshToken);
await user.save();
res.status(201).json({
message: 'User created successfully',
accessToken,
refreshToken,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
};
exports.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' });
}
// Check password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate tokens
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Save refresh token
user.refreshTokens.push(refreshToken);
await user.save();
res.json({
accessToken,
refreshToken,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
};
// server/src/middleware/auth.js
const jwt = require('jsonwebtoken');
exports.authenticate = async (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ message: 'Token expired' });
}
res.status(401).json({ message: 'Invalid token' });
}
};
exports.authorize = (...roles) => {
return async (req, res, next) => {
const user = await User.findById(req.userId);
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
};
};
Frontend: Auth Context & API Integration
// client/src/services/api.ts
import axios from 'axios';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api';
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post(`${API_URL}/auth/refresh`, {
refreshToken
});
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
// client/src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import api from '../services/api';
interface User {
id: string;
username: string;
email: string;
role: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise
;
register: (username: string, email: string, password: string) => Promise;
logout: () => void;
}
const AuthContext = createContext(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
const token = localStorage.getItem('accessToken');
if (token) {
try {
const response = await api.get('/auth/me');
setUser(response.data.user);
} catch (error) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}
setLoading(false);
};
const login = async (email: string, password: string) => {
const response = await api.post('/auth/login', { email, password });
const { accessToken, refreshToken, user } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
};
const register = async (username: string, email: string, password: string) => {
const response = await api.post('/auth/register', {
username,
email,
password
});
const { accessToken, refreshToken, user } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
};
const logout = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setUser(null);
};
return (
{children}
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
3. Database Integration
MongoDB with Mongoose
// server/src/config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB connected successfully');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;
// server/src/models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
maxlength: 200
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [String],
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
comments: [{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
text: String,
createdAt: {
type: Date,
default: Date.now
}
}],
published: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
// Indexes for better query performance
postSchema.index({ author: 1, createdAt: -1 });
postSchema.index({ tags: 1 });
postSchema.index({ title: 'text', content: 'text' });
module.exports = mongoose.model('Post', postSchema);
// server/src/controllers/postController.js
const Post = require('../models/Post');
exports.createPost = async (req, res) => {
try {
const { title, content, tags } = req.body;
const post = new Post({
title,
content,
tags,
author: req.userId
});
await post.save();
await post.populate('author', 'username email');
res.status(201).json(post);
} catch (error) {
res.status(500).json({ message: 'Error creating post', error: error.message });
}
};
exports.getPosts = async (req, res) => {
try {
const { page = 1, limit = 10, tag, search } = req.query;
const query = { published: true };
if (tag) {
query.tags = tag;
}
if (search) {
query.$text = { $search: search };
}
const posts = await Post.find(query)
.populate('author', 'username email')
.sort({ createdAt: -1 })
.limit(limit * 1)
.skip((page - 1) * limit)
.exec();
const count = await Post.countDocuments(query);
res.json({
posts,
totalPages: Math.ceil(count / limit),
currentPage: page
});
} catch (error) {
res.status(500).json({ message: 'Error fetching posts', error: error.message });
}
};
4. API Development
RESTful API Routes
// server/src/routes/posts.js
const express = require('express');
const router = express.Router();
const postController = require('../controllers/postController');
const { authenticate, authorize } = require('../middleware/auth');
const { validatePost } = require('../middleware/validation');
// Public routes
router.get('/', postController.getPosts);
router.get('/:id', postController.getPostById);
// Protected routes
router.use(authenticate);
router.post('/', validatePost, postController.createPost);
router.put('/:id', validatePost, postController.updatePost);
router.delete('/:id', postController.deletePost);
router.post('/:id/like', postController.likePost);
router.post('/:id/comment', postController.addComment);
// Admin only routes
router.patch('/:id/publish', authorize('admin'), postController.publishPost);
module.exports = router;
// server/src/middleware/validation.js
const { body, validationResult } = require('express-validator');
exports.validatePost = [
body('title')
.trim()
.isLength({ min: 3, max: 200 })
.withMessage('Title must be between 3 and 200 characters'),
body('content')
.trim()
.isLength({ min: 10 })
.withMessage('Content must be at least 10 characters'),
body('tags')
.optional()
.isArray()
.withMessage('Tags must be an array'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
// server/src/server.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const connectDB = require('./config/database');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Connect to database
connectDB();
// Middleware
app.use(helmet());
app.use(cors({
origin: process.env.CLIENT_URL,
credentials: true
}));
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/posts', require('./routes/posts'));
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date() });
});
// Error handling
app.use(errorHandler);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
5. Real-time Features
WebSocket with Socket.io
// server/src/socket.js
const socketIO = require('socket.io');
const jwt = require('jsonwebtoken');
const initializeSocket = (server) => {
const io = socketIO(server, {
cors: {
origin: process.env.CLIENT_URL,
credentials: true
}
});
// Authentication middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication error'));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('Authentication error'));
}
});
io.on('connection', (socket) => {
console.log('User connected:', socket.userId);
// Join user's personal room
socket.join(`user:${socket.userId}`);
// Chat functionality
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user-joined', { userId: socket.userId });
});
socket.on('send-message', async (data) => {
const { roomId, message } = data;
// Save message to database
const savedMessage = await Message.create({
room: roomId,
user: socket.userId,
text: message
});
// Broadcast to room
io.to(roomId).emit('new-message', savedMessage);
});
socket.on('typing', (roomId) => {
socket.to(roomId).emit('user-typing', { userId: socket.userId });
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.userId);
});
});
return io;
};
module.exports = initializeSocket;
// client/src/hooks/useSocket.ts
import { useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';
export const useSocket = (url: string, token: string) => {
const socketRef = useRef(null);
useEffect(() => {
socketRef.current = io(url, {
auth: { token }
});
socketRef.current.on('connect', () => {
console.log('Connected to socket server');
});
socketRef.current.on('disconnect', () => {
console.log('Disconnected from socket server');
});
return () => {
socketRef.current?.disconnect();
};
}, [url, token]);
return socketRef.current;
};
6. Deployment
Docker Configuration
# docker-compose.yml
version: '3.8'
services:
client:
build:
context: ./client
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- VITE_API_URL=http://localhost:5000/api
depends_on:
- server
server:
build:
context: ./server
dockerfile: Dockerfile
ports:
- "5000:5000"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/fullstack
- JWT_SECRET=${JWT_SECRET}
- REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET}
depends_on:
- mongo
- redis
mongo:
image: mongo:6
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
redis:
image: redis:7-alpine
ports:
- "6379:6379"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- client
- server
volumes:
mongo-data:
# server/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 5000
CMD ["node", "src/server.js"]
# client/Dockerfile
FROM node:18-alpine as build
DIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
7. Complete Project Example
Building a Blog Platform
Let's build a complete blog platform with all the features we've learned.
Features
- User authentication (register/login)
- Create, edit, delete posts
- Comment system
- Like/unlike posts
- Real-time notifications
- Search functionality
- User profiles
- Admin dashboard
Technical Stack
- React + TypeScript + Vite
- Node.js + Express
- MongoDB + Mongoose
- Socket.io for real-time
- JWT authentication
- Docker deployment
⚠️ Best Practices
- Use environment variables for configuration
- Implement proper error handling
- Add input validation on both frontend and backend
- Use HTTPS in production
- Implement rate limiting
- Add logging and monitoring
- Write tests for critical functionality
- Use TypeScript for type safety
Practice Exercises
Create a complete task management application:
- User authentication with JWT
- CRUD operations for tasks
- Task categories and priorities
- Real-time updates with Socket.io
- Search and filter functionality
- Responsive design
- Docker deployment
Build a mini e-commerce platform:
- Product catalog with search
- Shopping cart functionality
- User authentication
- Order management
- Payment integration (Stripe test mode)
- Admin dashboard
Create a social media dashboard:
- User profiles and authentication
- Post creation with images
- Follow/unfollow users
- Real-time notifications
- News feed with pagination
- Like and comment system
🎉 Summary
Congratulations! You've completed the Full Stack Development course! You now know:
- Full stack architecture and project structure
- Authentication and authorization implementation
- Database design and integration
- RESTful API development
- Real-time features with WebSockets
- Deployment with Docker and CI/CD
- Full stack best practices
Total points available: 10/10
🎓 Next Steps
Continue your learning journey:
- Build your own projects
- Contribute to open source
- Learn advanced topics (GraphQL, Microservices)
- Explore cloud platforms (AWS, Azure, GCP)
- Study system design and architecture
- Keep up with new technologies