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).
MongoDB MongoDB
Express Express
React React
Node.js Node.js

🛠️ 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

  1. Create a backend folder, run npm init -y.
  2. Install all dependencies:
    npm install express mongoose cors dotenv bcryptjs jsonwebtoken
  3. 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.js
const 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.

backend/routes/auth.js
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.

backend/middleware/auth.js
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.

backend/routes/posts.js
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.

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

  1. Create frontend app with npx create-react-app frontend.
  2. 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.

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.

frontend/src/index.js
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.

frontend/src/api.js
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.js
import 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.