Skip to main content
Express.js Tutorial
CHAPTER 18 Beginner

Building a Complete Express.js Project

Updated: May 14, 2026
45 min read

# CHAPTER 18

Building a Complete Express.js Project

1. Introduction

Theoretical knowledge is useless without practical application. In this chapter, we will synthesize everything we have learned—Express Routing, Mongoose Models, Controllers, JWT Authentication, Multer Uploads, and Security Middleware—to architect a real-world project: a multi-user Blogging API. This project mimics the exact technical requirements you will face in a professional backend engineering role.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Architect a complete Node.js/Express MVC directory structure.
  • Design related Mongoose schemas (User and Post).
  • Implement an end-to-end authentication flow.
  • Restrict data access so users can only edit their own posts.

3. Project Overview: The Blog API

Requirements:
  • Users must be able to register, upload an avatar (Multer), and log in to receive a JWT.
  • Logged-in users can Create, Read, Update, and Delete (CRUD) blog posts.
  • A post has a title, content, and a linked author.
  • Crucial Security Rule: User A cannot edit or delete User B's posts.

4. Step 1: The Project Architecture

A professional Express API separates concerns. Create this folder structure:
text
123456789
/my-blog-api
  /config      (db.js)
  /controllers (authController.js, postController.js)
  /middleware  (auth.js, upload.js)
  /models      (User.js, Post.js)
  /routes      (authRoutes.js, postRoutes.js)
  /public      (/uploads)
  index.js
  .env

5. Step 2: The Mongoose Models

models/Post.js Notice how we link the Post to a specific User using ObjectId.
javascript
1234567891011
const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
    // Establish the relationship: This post belongs to a specific User
    author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
    title: { type: String, required: true },
    content: { type: String, required: true },
    isPublished: { type: Boolean, default: true }
}, { timestamps: true });

module.exports = mongoose.model('Post', postSchema);

*(Assume models/User.js exists as created in Chapter 11).*

6. Step 3: The Post Controller (Business Logic)

Here we enforce the security rules.

controllers/postController.js

javascript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
const Post = require('../models/Post');

// CREATE POST
exports.createPost = async (req, res) => {
    try {
        // We do NOT trust the user to send their ID in req.body. 
        // We pull it securely from the JWT attached by the Auth middleware!
        const newPost = new Post({
            author: req.user.id, 
            title: req.body.title,
            content: req.body.content
        });

        await newPost.save();
        res.status(201).json({ status: "success", data: newPost });
    } catch (err) {
        res.status(400).json({ error: err.message });
    }
};

// GET ALL POSTS (Public)
exports.getPosts = async (req, res) => {
    try {
        // Find all posts and 'populate' the author field with the User's name!
        const posts = await Post.find({ isPublished: true }).populate('author', 'name');
        res.status(200).json({ status: "success", data: posts });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
};

// DELETE POST (Secure)
exports.deletePost = async (req, res) => {
    try {
        const post = await Post.findById(req.params.id);
        if (!post) return res.status(404).json({ error: "Post not found" });

        // SECURITY CHECK: Does the logged-in user own this specific post?
        if (post.author.toString() !== req.user.id) {
            return res.status(403).json({ error: "Forbidden. You do not own this post." });
        }

        await post.deleteOne();
        res.status(200).json({ message: "Post deleted successfully" });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
};

7. Step 4: The Routes

We map the URLs to the controller functions, and inject the verifyToken middleware selectively.

routes/postRoutes.js

javascript
12345678910111213
const express = require('express');
const router = express.Router();
const postController = require('../controllers/postController');
const verifyToken = require('../middleware/auth'); // From Chapter 13

// Public Route (No Token needed)
router.get('/', postController.getPosts);

// Protected Routes (Require Token)
router.post('/', verifyToken, postController.createPost);
router.delete('/:id', verifyToken, postController.deletePost);

module.exports = router;

8. Step 5: Wiring the Server

Bring it all together securely.

index.js

javascript
12345678910111213141516171819202122232425262728
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const helmet = require('helmet');
const postRoutes = require('./routes/postRoutes');
const authRoutes = require('./routes/authRoutes');

const app = express();

// Security and Parsers
app.use(helmet());
app.use(express.json());
app.use(express.static('public')); // Serve uploaded avatars

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/posts', postRoutes);

// Global Error Handler
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: "Internal Server Error" });
});

// Boot the server
mongoose.connect(process.env.MONGO_URI).then(() => {
    app.listen(process.env.PORT || 3000, () => console.log("API Running"));
});

9. Reviewing the Architecture

Look closely at the createPost controller method. We did not write author: req.body.userId. If we did, a hacker could change the JSON payload to userId: 5 and publish posts on someone else's account! Instead, we ignored the JSON body entirely for the ID. We forcefully grabbed the ID securely from req.user.id (which was decrypted from the unforgeable JWT token). This is the hallmark of professional backend security.

10. Summary

You just built a multi-user Software-as-a-Service (SaaS) backend!
  1. 1. Models established the relational constraints between Users and Posts.
  1. 2. Auth Middleware protected the routes and identified the user.
  1. 3. Controllers handled the database queries and enforced strict ownership authorization.
  1. 4. Express Routers orchestrated the traffic elegantly.
You now have a portfolio-ready API that can power a React website or an iOS app.

11. Next Chapter Recommendation

Your application is brilliant, but it only exists on your laptop. It's time to show it to the world. Proceed to Chapter 19: Deploying Express.js Applications.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·