Project Idea: Full Stack Blog
A complete blog where users can register, log in, create their own posts, and view posts from others. This is a fantastic portfolio project that demonstrates all the core full-stack skills: authentication, database relationships, and full CRUD operations.
✨ Key Features
- Secure user registration and login with JWT (JSON Web Tokens).
- Full Create, Read, Update, Delete (CRUD) functionality for blog posts.
- Protected routes: Only logged-in users can create posts.
- Authorization: Only the original author of a post can edit or delete it.
- A public homepage to read all posts.
💻 Tech Stack & Skills
- Tech Stack: MERN (MongoDB, Express, React, Node.js), `bcryptjs` (Password Hashing), `jsonwebtoken` (Auth Tokens), `react-router-dom`.
- Skills Practiced: Full-stack CRUD API, User Authentication (AuthN), User Authorization (AuthZ), Password Hashing, JWT handling, Mongoose Relationships (`ref`), React Context API (for Auth).
🛠️ Full Step-by-Step Tutorial
This is a large project. We will build the backend API first, then the frontend React app.
Part 1: The Backend API (Node.js & Express)
Step 1.1: Setup Project
- Create a
backendfolder, runnpm init -y. - Install all dependencies:
npm install express mongoose cors dotenv bcryptjs jsonwebtoken - Install dev dependency:
npm install -D nodemon
Step 1.2: Create Models (User & Post)
This is crucial. We need to link posts to users.
backend/models/User.jsconst mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
});
module.exports = mongoose.model('User', userSchema);
backend/models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
content: { type: String, required: true },
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // This creates the link!
required: true
},
createdAt: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Post', postSchema);
Step 1.3: Create Auth Routes (Signup/Login)
Create backend/routes/auth.js. This handles user accounts.
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// SIGNUP
router.post('/signup', async (req, res) => {
try {
const { username, email, password } = req.body;
// Hash the password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Create new user
const newUser = new User({ username, email, password: hashedPassword });
await newUser.save();
res.status(201).json("User created successfully");
} catch (err) {
res.status(500).json(err);
}
});
// LOGIN
router.post('/login', async (req, res) => {
try {
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(404).json("User not found");
}
const isMatch = await bcrypt.compare(req.body.password, user.password);
if (!isMatch) {
return res.status(400).json("Wrong credentials");
}
// Create JWT
const token = jwt.sign(
{ id: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Send token and user data (except password)
const { password, ...others } = user._doc;
res.json({ ...others, token });
} catch (err) {
res.status(500).json(err);
}
});
module.exports = router;
Step 1.4: Create Auth Middleware (The Gatekeeper)
Create backend/middleware/auth.js. This function will protect our routes.
const jwt = require('jsonwebtoken');
const auth = (req, res, next) => {
const authHeader = req.header('Authorization');
if (!authHeader) {
return res.status(401).json("Access denied. No token provided.");
}
// Token format is "Bearer "
const token = authHeader.split(' ')[1];
if (!token) {
return res.status(401).json("Access denied. No token provided.");
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Add user payload (id, username) to request
next(); // Move to the next function
} catch (err) {
res.status(400).json("Invalid token.");
}
};
module.exports = auth;
Step 1.5: Create Post Routes (CRUD)
Create backend/routes/posts.js. Notice how we use the auth middleware.
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
const auth = require('../middleware/auth'); // Our auth middleware
// GET ALL POSTS (Public)
router.get('/', async (req, res) => {
try {
const posts = await Post.find().populate('author', 'username').sort({ createdAt: -1 });
res.json(posts);
} catch (err) { res.status(500).json(err); }
});
// GET SINGLE POST (Public)
router.get('/:id', async (req, res) => {
try {
const post = await Post.findById(req.params.id).populate('author', 'username');
res.json(post);
} catch (err) { res.status(500).json(err); }
});
// CREATE NEW POST (Protected)
router.post('/', auth, async (req, res) => {
const newPost = new Post({
title: req.body.title,
content: req.body.content,
author: req.user.id, // From auth middleware
});
try {
const savedPost = await newPost.save();
res.status(201).json(savedPost);
} catch (err) { res.status(500).json(err); }
});
// UPDATE POST (Protected & Authorized)
router.patch('/:id', auth, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json("Post not found");
// Authorization check: Is this user the author?
if (post.author.toString() !== req.user.id) {
return res.status(403).json("You can only update your own posts");
}
const updatedPost = await Post.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
);
res.json(updatedPost);
} catch (err) { res.status(500).json(err); }
});
// DELETE POST (Protected & Authorized)
router.delete('/:id', auth, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json("Post not found");
// Authorization check
if (post.author.toString() !== req.user.id) {
return res.status(403).json("You can only delete your own posts");
}
await post.delete();
res.json("Post has been deleted");
} catch (err) { res.status(500).json(err); }
});
module.exports = router;
Step 1.6: Server (`index.js`)
Finally, tie it all together in backend/index.js.
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
const app = express();
const PORT = process.env.PORT || 5000;
// Middlewares
app.use(cors());
app.use(express.json());
// DB Connect
mongoose.connect(process.env.MONGO_URI, { /* ... options ... */ })
.then(() => console.log('MongoDB Connected'))
.catch(err => console.log(err));
// Use Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/posts', require('./routes/posts'));
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Don't forget to add MONGO_URI and JWT_SECRET to your .env file!
Part 2: The Frontend (React)
Step 2.1: Setup Project
- Create
frontendapp withnpx create-react-app frontend. - Install libraries:
npm install axios react-router-dom
Step 2.2: Create `AuthContext.js`
This is vital for managing login state. Create frontend/src/context/AuthContext.js.
import React, { createContext, useContext, useState, useEffect } from 'react';
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem('token'));
useEffect(() => {
// Persist user on page reload
const storedUser = localStorage.getItem('user');
if (storedUser && token) {
setUser(JSON.parse(storedUser));
}
}, [token]);
const loginAction = (data) => {
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data)); // Save user data
setToken(data.token);
setUser(data);
};
const logoutAction = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
setToken(null);
setUser(null);
};
return (
{children}
);
};
export const useAuth = () => {
return useContext(AuthContext);
};
Step 2.3: Wrap App in Providers
In frontend/src/index.js, wrap App.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from './context/AuthContext';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
Step 2.4: Create an API helper (Best Practice)
Create frontend/src/api.js to automatically add our token to requests.
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:5000/api', // Your backend URL
});
// Add a request interceptor to include the 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);
}
);
export default api;
Step 2.5: Create Components & Routing
Now just create your components (`HomePage`, `LoginPage`, `CreatePostPage`, etc.) and use `useAuth()` hook and the `api` helper.
Example: frontend/src/components/LoginPage.jsimport React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import api from '../api'; // Our custom axios instance
import { useNavigate } from 'react-router-dom';
function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { loginAction } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await api.post('/auth/login', { email, password });
loginAction(response.data); // This saves token and user
navigate('/'); // Redirect to homepage
} catch (err) {
setError("Failed to login. Check credentials.");
}
};
// ... return a form JSX here ...
}
Example: frontend/src/components/CreatePostPage.js
import React, { useState } from 'react';
import api from '../api'; // This API helper automatically sends the token
import { useNavigate } from 'react-router-dom';
function CreatePostPage() {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
// The auth token is sent automatically by our api helper
const response = await api.post('/posts', { title, content });
navigate(`/posts/${response.data._id}`); // Go to new post
} catch (err) {
console.error("Failed to create post", err);
}
};
// ... return a form JSX here ...
}
🔥 Next Steps & Challenges
After completing the base project, challenge yourself with these features:
- **Comment System:** Allow logged-in users to comment on posts (this requires a new `Comment` model in the backend).
- **Rich Text Editor:** Replace the simple `textarea` with a rich text editor like `React Quill` or `TipTap` to allow bold, italics, and lists.
- **File Uploads:** Allow authors to upload a "cover image" for their blog post (this requires using `multer` on the backend).
- **Categories & Tags:** Add the ability to categorize posts and filter by tag.