🚀 Full Stack Development Course

🎯 Learning Objectives

After completing this course, you will be able to:

⚡ 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

Practice Exercises

📝 Exercise 1: Build a Task Management App (5 points)

Create a complete task management application:

📝 Exercise 2: E-commerce Platform (3 points)

Build a mini e-commerce platform:

📝 Exercise 3: Social Media Dashboard (2 points)

Create a social media dashboard:

🎉 Summary

Congratulations! You've completed the Full Stack Development course! You now know:

Total points available: 10/10

🎓 Next Steps

Continue your learning journey:

🏠 Course Home

Back to Course 1

WORK