Mini Full-Stack Project (Blog/E-commerce API)
Congratulations! You've learned all the core concepts of Express. Now it's time to build something real – a mini full-stack project that brings everything together. We'll build a blog platform API that can be extended to an e-commerce site with minor modifications.
Project Overview: Blog API
We'll build a blog platform with these features:
- User registration and login (JWT authentication)
- User profiles with bio and avatar upload
- Blog posts with CRUD operations
- Comments on posts
- Categories and tags
- Likes/bookmarks
- Search and filtering
- Pagination
- Rate limiting
- File uploads for avatars and post images
Project Structure
blog-api/
├── src/
│ ├── config/
│ │ ├── config.js
│ │ ├── db.js
│ │ └── cloudinary.js (for image uploads)
│ ├── models/
│ │ ├── User.js
│ │ ├── Post.js
│ │ ├── Comment.js
│ │ ├── Category.js
│ │ └── Like.js
│ ├── controllers/
│ │ ├── authController.js
│ │ ├── userController.js
│ │ ├── postController.js
│ │ ├── commentController.js
│ │ └── categoryController.js
│ ├── routes/
│ │ ├── authRoutes.js
│ │ ├── userRoutes.js
│ │ ├── postRoutes.js
│ │ ├── commentRoutes.js
│ │ └── categoryRoutes.js
│ ├── middleware/
│ │ ├── auth.js
│ │ ├── upload.js (multer config)
│ │ ├── validation.js
│ │ └── rateLimiter.js
│ ├── utils/
│ │ ├── AppError.js
│ │ ├── catchAsync.js
│ │ └── apiFeatures.js
│ └── app.js
├── .env
├── package.json
└── README.mdKey Models
User Model (enhanced)
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
bio: { type: String, maxlength: 500 },
avatar: { type: String, default: 'default-avatar.jpg' },
website: String,
location: String,
role: { type: String, enum: ['user', 'admin'], default: 'user' },
isActive: { type: Boolean, default: true },
lastLogin: Date,
passwordChangedAt: Date,
passwordResetToken: String,
passwordResetExpires: Date
}, { timestamps: true });Post Model (enhanced)
const postSchema = new mongoose.Schema({
title: { type: String, required: true, index: true },
slug: { type: String, unique: true, required: true },
content: { type: String, required: true },
excerpt: { type: String, maxlength: 300 },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' },
tags: [String],
featuredImage: String,
published: { type: Boolean, default: false },
publishedAt: Date,
views: { type: Number, default: 0 },
likesCount: { type: Number, default: 0 },
commentsCount: { type: Number, default: 0 }
}, { timestamps: true });
// Text search index
postSchema.index({ title: 'text', content: 'text' });Comment Model
const commentSchema = new mongoose.Schema({
content: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
parentComment: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }, // For nested comments
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
isEdited: { type: Boolean, default: false }
}, { timestamps: true });Category Model
const categorySchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
slug: { type: String, required: true, unique: true },
description: String,
parentCategory: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' } // For subcategories
});API Features Utility (utils/apiFeatures.js)
class APIFeatures {
constructor(query, queryString) {
this.query = query;
this.queryString = queryString;
}
filter() {
const queryObj = { ...this.queryString };
const excludedFields = ['page', 'sort', 'limit', 'fields', 'search'];
excludedFields.forEach(el => delete queryObj[el]);
// Advanced filtering (gt, gte, lt, lte)
let queryStr = JSON.stringify(queryObj);
queryStr = queryStr.replace(/b(gte|gt|lte|lt)b/g, match => `$${match}`);
this.query = this.query.find(JSON.parse(queryStr));
return this;
}
search() {
if (this.queryString.search) {
this.query = this.query.find({
$text: { $search: this.queryString.search }
});
}
return this;
}
sort() {
if (this.queryString.sort) {
const sortBy = this.queryString.sort.split(',').join(' ');
this.query = this.query.sort(sortBy);
} else {
this.query = this.query.sort('-createdAt');
}
return this;
}
limitFields() {
if (this.queryString.fields) {
const fields = this.queryString.fields.split(',').join(' ');
this.query = this.query.select(fields);
} else {
this.query = this.query.select('-__v');
}
return this;
}
paginate() {
const page = parseInt(this.queryString.page) || 1;
const limit = parseInt(this.queryString.limit) || 10;
const skip = (page - 1) * limit;
this.query = this.query.skip(skip).limit(limit);
return this;
}
}
module.exports = APIFeatures;Post Controller with API Features
const Post = require('../models/Post');
const APIFeatures = require('../utils/apiFeatures');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/AppError');
exports.getAllPosts = catchAsync(async (req, res) => {
// Build query
const features = new APIFeatures(Post.find(), req.query)
.search()
.filter()
.sort()
.limitFields()
.paginate();
// Execute query
const posts = await features.query.populate('author', 'name avatar');
const total = await Post.countDocuments();
res.json({
success: true,
results: posts.length,
total,
page: parseInt(req.query.page) || 1,
pages: Math.ceil(total / (parseInt(req.query.limit) || 10)),
data: posts
});
});
exports.getPost = catchAsync(async (req, res, next) => {
const post = await Post.findById(req.params.id)
.populate('author', 'name avatar')
.populate({
path: 'comments',
populate: { path: 'author', select: 'name avatar' }
});
if (!post) {
return next(new AppError('Post not found', 404));
}
// Increment view count
post.views += 1;
await post.save({ validateBeforeSave: false });
res.json({ success: true, data: post });
});File Upload with Cloudinary (config/cloudinary.js)
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
const multer = require('multer');
// Configure Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
// Configure storage
const storage = new CloudinaryStorage({
cloudinary: cloudinary,
params: {
folder: 'blog-api',
allowed_formats: ['jpg', 'jpeg', 'png', 'gif'],
transformation: [{ width: 1000, height: 1000, crop: 'limit' }]
}
});
const upload = multer({ storage });
module.exports = { cloudinary, upload };Rate Limiting for API
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many attempts, please try again after 15 minutes'
});
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Rate limit exceeded, please slow down'
});
module.exports = { authLimiter, apiLimiter };Extending to E-commerce
To convert this blog API to an e-commerce API, you would:
- Create Product model instead of Post model (with price, stock, SKU, etc.)
- Add Order and Cart models
- Add payment processing (Stripe, PayPal)
- Add shipping information
- Add inventory management
- Add reviews and ratings instead of comments
Two Minute Drill
- This mini full-stack project combines everything you've learned.
- Use proper project structure for scalability.
- Implement advanced features like search, filtering, and pagination.
- Use Cloudinary for image uploads (better than local storage).
- Add rate limiting to protect your API.
- This structure can be easily adapted for e-commerce.
- Now you're ready to build real-world applications!
Need more clarification?
Drop us an email at career@quipoinfotech.com
