Project Overview
In this weekend project, you'll build a complete RESTful API for a blog application with database integration. This project will bring together everything you've learned about Node.js, Express, and databases to create a functional backend system that can serve as the foundation for a full-stack blog application.
By the end of this project, you will have:
- A fully functional RESTful API for a blog application
- Database integration with either MongoDB (NoSQL) or PostgreSQL (SQL)
- User authentication and authorization
- CRUD operations for blog posts, comments, and users
- Data validation and error handling
- API testing with appropriate tools
George Polya's Problem Solving Approach for Our Project
We'll use George Polya's famous four-step problem-solving method to tackle this project:
Step 1: Understand the Problem
Define the requirements and features of the blog API. What entities do we need? What operations should be supported? What are the constraints and expectations?
Step 2: Devise a Plan
Outline the architecture and components of the API. Design the database schema. Plan the routes and controllers.
Step 3: Execute the Plan
Implement the API step-by-step according to the plan. Set up the database, create models, implement routes, add authentication, etc.
Step 4: Review and Extend
Test the API, review the implementation, identify areas for improvement, and extend functionality as needed.
Step 1: Understand the Problem
Project Requirements
Before we start coding, let's understand what we're building and what features our blog API should have:
Core Entities
- Users: People who can create and manage blog content
- Posts: Blog articles with title, content, author, etc.
- Comments: User feedback on specific posts
- Categories: Topics that posts can belong to
Required Operations
- User Management: Registration, authentication, profile management
- Post Management: Create, read, update, delete posts
- Comment Management: Add, view, moderate comments
- Category Management: Create, list, and filter by categories
Technical Requirements
- RESTful API design principles
- JSON data format for requests and responses
- JWT-based authentication
- Database persistence (either MongoDB or PostgreSQL)
- Proper error handling and validation
- Documentation with API endpoints
Real-World Scenario
Imagine you're a developer at a digital media company that wants to create a new content platform. They need a flexible backend that multiple frontend applications (web, mobile apps) can use to access and manipulate blog content. The API will be the central component that ensures all platforms have consistent data and behavior.
Step 2: Devise a Plan
Architecture Overview
Let's design a high-level architecture for our blog API:
Project Structure Plan
blog-api/
│
├── config/ # Configuration files
│ ├── db.js # Database connection setup
│ └── auth.js # Authentication configuration
│
├── models/ # Database models
│ ├── User.js
│ ├── Post.js
│ ├── Comment.js
│ └── Category.js
│
├── routes/ # API routes
│ ├── auth.js # Authentication routes
│ ├── users.js # User-related routes
│ ├── posts.js # Post-related routes
│ ├── comments.js # Comment-related routes
│ └── categories.js # Category-related routes
│
├── controllers/ # Route handlers
│ ├── authController.js
│ ├── userController.js
│ ├── postController.js
│ ├── commentController.js
│ └── categoryController.js
│
├── middleware/ # Custom middleware
│ ├── auth.js # Authentication middleware
│ ├── validation.js # Request validation
│ └── errorHandler.js # Global error handler
│
├── utils/ # Utility functions
│ ├── validators.js
│ └── helpers.js
│
├── tests/ # API tests
│
├── app.js # Express application setup
├── server.js # Server entry point
├── package.json # Project dependencies
└── README.md # Project documentation
Database Schema Design
Let's design our database schema for both MongoDB (document-based) and PostgreSQL (relational) options:
Option 1: MongoDB Schema (using Mongoose)
Option 2: PostgreSQL Schema (using Knex.js or Sequelize)
Numbered Whiteboard Plan
- Set up project structure and install dependencies
- Configure database connection (MongoDB or PostgreSQL)
- Create database models for User, Post, Comment, Category
- Implement user authentication (registration, login, JWT)
- Create CRUD operations for Users
- Create CRUD operations for Posts
- Create CRUD operations for Comments
- Create CRUD operations for Categories
- Implement validation and error handling
- Add authorization (user roles, permissions)
- Write tests for API endpoints
- Document the API
API Endpoints Plan
Authentication Endpoints
POST /api/auth/register- Register a new userPOST /api/auth/login- Login and get JWT token
User Endpoints
GET /api/users- Get all users (admin only)GET /api/users/:id- Get user by IDPUT /api/users/:id- Update userDELETE /api/users/:id- Delete userGET /api/users/:id/posts- Get posts by user
Post Endpoints
GET /api/posts- Get all posts (with pagination)GET /api/posts/:id- Get post by IDPOST /api/posts- Create a new postPUT /api/posts/:id- Update postDELETE /api/posts/:id- Delete post
Comment Endpoints
GET /api/posts/:postId/comments- Get comments for a postPOST /api/posts/:postId/comments- Add comment to a postPUT /api/comments/:id- Update commentDELETE /api/comments/:id- Delete comment
Category Endpoints
GET /api/categories- Get all categoriesGET /api/categories/:id- Get category by IDPOST /api/categories- Create a new category (admin only)PUT /api/categories/:id- Update category (admin only)DELETE /api/categories/:id- Delete category (admin only)GET /api/categories/:id/posts- Get posts by category
Step 3: Execute the Plan
Now let's implement our blog API step by step, following the plan we've devised. For this tutorial, we'll use MongoDB with Mongoose as our database solution, but the concepts can be adapted for PostgreSQL with minimal changes.
Step 3.1: Set Up Project Structure and Install Dependencies
First, let's create our project directory and initialize it:
# Create project directory
mkdir blog-api
cd blog-api
# Initialize npm project
npm init -y
# Install dependencies
npm install express mongoose bcryptjs jsonwebtoken dotenv express-validator cors helmet morgan
# Install development dependencies
npm install -D nodemon
Let's update the package.json file to add some useful scripts:
File: package.json (partial)
{
"name": "blog-api",
"version": "1.0.0",
"description": "Blog API with MongoDB integration",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ... rest of the file
}
Now, let's create our basic project structure:
# Create directory structure
mkdir config models routes controllers middleware utils tests
# Create main files
touch server.js app.js
touch .env .gitignore README.md
# Create config files
touch config/db.js config/auth.js
# Create model files
touch models/User.js models/Post.js models/Comment.js models/Category.js
# Create route files
touch routes/auth.js routes/users.js routes/posts.js routes/comments.js routes/categories.js
# Create controller files
touch controllers/authController.js controllers/userController.js controllers/postController.js controllers/commentController.js controllers/categoryController.js
# Create middleware files
touch middleware/auth.js middleware/validation.js middleware/errorHandler.js
Add the following to your .gitignore file:
File: .gitignore
node_modules/
.env
.DS_Store
npm-debug.log
coverage/
.vscode/
Create a .env file for environment variables:
File: .env
NODE_ENV=development
PORT=5000
MONGO_URI=mongodb://localhost:27017/blog_api
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRE=30d
Step 3.2: Configure Database Connection
Let's set up our MongoDB connection:
File: config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
// Connect to MongoDB
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Step 3.3: Create Database Models
Now let's create our Mongoose models for each entity:
File: models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Please add a username'],
unique: true,
trim: true,
maxlength: [50, 'Username cannot be more than 50 characters']
},
email: {
type: String,
required: [true, 'Please add an email'],
unique: true,
match: [
/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
'Please add a valid email'
]
},
password: {
type: String,
required: [true, 'Please add a password'],
minlength: [6, 'Password must be at least 6 characters'],
select: false // Don't return password by default
},
name: {
type: String,
trim: true
},
bio: {
type: String,
maxlength: [500, 'Bio cannot be more than 500 characters']
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true
});
// Encrypt password using bcrypt
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
// Sign JWT and return
UserSchema.methods.getSignedJwtToken = function() {
return jwt.sign({ id: this._id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
};
// Match user entered password to hashed password in database
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
File: models/Category.js
const mongoose = require('mongoose');
const slugify = require('slugify');
const CategorySchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please add a name'],
unique: true,
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
slug: {
type: String,
unique: true
},
description: {
type: String,
maxlength: [500, 'Description cannot be more than 500 characters']
}
}, {
timestamps: true
});
// Create slug from name
CategorySchema.pre('save', function(next) {
this.slug = slugify(this.name, { lower: true });
next();
});
module.exports = mongoose.model('Category', CategorySchema);
File: models/Post.js
const mongoose = require('mongoose');
const slugify = require('slugify');
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Please add a title'],
trim: true,
maxlength: [200, 'Title cannot be more than 200 characters']
},
slug: {
type: String,
unique: true
},
content: {
type: String,
required: [true, 'Please add content']
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
categories: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
}],
status: {
type: String,
enum: ['draft', 'published'],
default: 'draft'
},
featuredImage: {
type: String
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Create slug from title
PostSchema.pre('save', function(next) {
this.slug = slugify(this.title, { lower: true });
next();
});
// Reverse populate with virtuals
PostSchema.virtual('comments', {
ref: 'Comment',
localField: '_id',
foreignField: 'post',
justOne: false
});
module.exports = mongoose.model('Post', PostSchema);
File: models/Comment.js
const mongoose = require('mongoose');
const CommentSchema = new mongoose.Schema({
content: {
type: String,
required: [true, 'Please add some content'],
maxlength: [1000, 'Comment cannot be more than 1000 characters']
},
post: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Post',
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Comment', CommentSchema);
Step 3.4: Set Up Express Application
Let's create our Express application:
File: app.js
const express = require('express');
const dotenv = require('dotenv');
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');
const errorHandler = require('./middleware/errorHandler');
// Load env vars
dotenv.config();
// Connect to database
const connectDB = require('./config/db');
connectDB();
// Route files
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');
const commentRoutes = require('./routes/comments');
const categoryRoutes = require('./routes/categories');
const app = express();
// Body parser
app.use(express.json());
// Dev logging middleware
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// Security middleware
app.use(helmet());
app.use(cors());
// Mount routers
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);
app.use('/api/comments', commentRoutes);
app.use('/api/categories', categoryRoutes);
// Error handler middleware
app.use(errorHandler);
module.exports = app;
File: server.js
const app = require('./app');
const PORT = process.env.PORT || 5000;
const server = app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
console.log(`Error: ${err.message}`);
// Close server & exit process
server.close(() => process.exit(1));
});
Step 3.5: Implement Authentication Middleware
Let's create the middleware for protecting routes:
File: middleware/auth.js
const jwt = require('jsonwebtoken');
const asyncHandler = require('../utils/asyncHandler');
const ErrorResponse = require('../utils/errorResponse');
const User = require('../models/User');
// Protect routes
exports.protect = asyncHandler(async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
// Set token from Bearer token
token = req.headers.authorization.split(' ')[1];
}
// Make sure token exists
if (!token) {
return next(new ErrorResponse('Not authorized to access this route', 401));
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user to request object
req.user = await User.findById(decoded.id);
next();
} catch (err) {
return next(new ErrorResponse('Not authorized to access this route', 401));
}
});
// Grant access to specific roles
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return next(
new ErrorResponse(
`User role ${req.user.role} is not authorized to access this route`,
403
)
);
}
next();
};
};
Step 3.6: Create Utility Functions
Let's create some utility functions that we'll need:
File: utils/errorResponse.js
class ErrorResponse extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
module.exports = ErrorResponse;
File: utils/asyncHandler.js
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;
File: middleware/errorHandler.js
const ErrorResponse = require('../utils/errorResponse');
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log to console for dev
console.log(err.stack);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = `Resource not found`;
error = new ErrorResponse(message, 404);
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = new ErrorResponse(message, 400);
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = new ErrorResponse(message, 400);
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error'
});
};
module.exports = errorHandler;
Step 3.7: Implement Authentication Controllers and Routes
Now let's create the authentication controllers and routes:
File: controllers/authController.js
const User = require('../models/User');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../utils/asyncHandler');
// @desc Register user
// @route POST /api/auth/register
// @access Public
exports.register = asyncHandler(async (req, res, next) => {
const { username, email, password, name } = req.body;
// Create user
const user = await User.create({
username,
email,
password,
name
});
sendTokenResponse(user, 201, res);
});
// @desc Login user
// @route POST /api/auth/login
// @access Public
exports.login = asyncHandler(async (req, res, next) => {
const { email, password } = req.body;
// Validate email & password
if (!email || !password) {
return next(new ErrorResponse('Please provide an email and password', 400));
}
// Check for user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return next(new ErrorResponse('Invalid credentials', 401));
}
// Check if password matches
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return next(new ErrorResponse('Invalid credentials', 401));
}
sendTokenResponse(user, 200, res);
});
// @desc Get current logged in user
// @route GET /api/auth/me
// @access Private
exports.getMe = asyncHandler(async (req, res, next) => {
const user = await User.findById(req.user.id);
res.status(200).json({
success: true,
data: user
});
});
// @desc Log user out / clear cookie
// @route GET /api/auth/logout
// @access Private
exports.logout = asyncHandler(async (req, res, next) => {
res.status(200).json({
success: true,
data: {}
});
});
// Helper function to get token from model, create cookie and send response
const sendTokenResponse = (user, statusCode, res) => {
// Create token
const token = user.getSignedJwtToken();
res.status(statusCode).json({
success: true,
token
});
};
File: routes/auth.js
const express = require('express');
const {
register,
login,
getMe,
logout
} = require('../controllers/authController');
const router = express.Router();
const { protect } = require('../middleware/auth');
router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
router.get('/logout', protect, logout);
module.exports = router;
Step 3.8: Implement User Controllers and Routes
File: controllers/userController.js
const User = require('../models/User');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../utils/asyncHandler');
// @desc Get all users
// @route GET /api/users
// @access Private/Admin
exports.getUsers = asyncHandler(async (req, res, next) => {
res.status(200).json(res.advancedResults);
});
// @desc Get single user
// @route GET /api/users/:id
// @access Private/Admin
exports.getUser = asyncHandler(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(
new ErrorResponse(`User not found with id of ${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: user
});
});
// @desc Create user
// @route POST /api/users
// @access Private/Admin
exports.createUser = asyncHandler(async (req, res, next) => {
const user = await User.create(req.body);
res.status(201).json({
success: true,
data: user
});
});
// @desc Update user
// @route PUT /api/users/:id
// @access Private/Admin
exports.updateUser = asyncHandler(async (req, res, next) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!user) {
return next(
new ErrorResponse(`User not found with id of ${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: user
});
});
// @desc Delete user
// @route DELETE /api/users/:id
// @access Private/Admin
exports.deleteUser = asyncHandler(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(
new ErrorResponse(`User not found with id of ${req.params.id}`, 404)
);
}
await user.remove();
res.status(200).json({
success: true,
data: {}
});
});
File: routes/users.js
const express = require('express');
const {
getUsers,
getUser,
createUser,
updateUser,
deleteUser
} = require('../controllers/userController');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
router
.route('/')
.get(protect, authorize('admin'), getUsers)
.post(protect, authorize('admin'), createUser);
router
.route('/:id')
.get(protect, authorize('admin'), getUser)
.put(protect, authorize('admin'), updateUser)
.delete(protect, authorize('admin'), deleteUser);
module.exports = router;
Step 3.9: Implement Post Controllers and Routes
File: controllers/postController.js
const Post = require('../models/Post');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../utils/asyncHandler');
// @desc Get all posts
// @route GET /api/posts
// @access Public
exports.getPosts = asyncHandler(async (req, res, next) => {
res.status(200).json(res.advancedResults);
});
// @desc Get single post
// @route GET /api/posts/:id
// @access Public
exports.getPost = asyncHandler(async (req, res, next) => {
const post = await Post.findById(req.params.id)
.populate({
path: 'author',
select: 'name username'
})
.populate({
path: 'categories',
select: 'name slug'
})
.populate({
path: 'comments',
select: 'content createdAt',
populate: {
path: 'author',
select: 'name username'
}
});
if (!post) {
return next(
new ErrorResponse(`Post not found with id of ${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: post
});
});
// @desc Create new post
// @route POST /api/posts
// @access Private
exports.createPost = asyncHandler(async (req, res, next) => {
// Add author to req.body
req.body.author = req.user.id;
const post = await Post.create(req.body);
res.status(201).json({
success: true,
data: post
});
});
// @desc Update post
// @route PUT /api/posts/:id
// @access Private
exports.updatePost = asyncHandler(async (req, res, next) => {
let post = await Post.findById(req.params.id);
if (!post) {
return next(
new ErrorResponse(`Post not found with id of ${req.params.id}`, 404)
);
}
// Make sure user is post author or admin
if (post.author.toString() !== req.user.id && req.user.role !== 'admin') {
return next(
new ErrorResponse(
`User ${req.user.id} is not authorized to update this post`,
401
)
);
}
post = await Post.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
res.status(200).json({
success: true,
data: post
});
});
// @desc Delete post
// @route DELETE /api/posts/:id
// @access Private
exports.deletePost = asyncHandler(async (req, res, next) => {
const post = await Post.findById(req.params.id);
if (!post) {
return next(
new ErrorResponse(`Post not found with id of ${req.params.id}`, 404)
);
}
// Make sure user is post author or admin
if (post.author.toString() !== req.user.id && req.user.role !== 'admin') {
return next(
new ErrorResponse(
`User ${req.user.id} is not authorized to delete this post`,
401
)
);
}
await post.remove();
res.status(200).json({
success: true,
data: {}
});
});
File: routes/posts.js
const express = require('express');
const {
getPosts,
getPost,
createPost,
updatePost,
deletePost
} = require('../controllers/postController');
// Include other resource routers
const commentRouter = require('./comments');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
// Re-route into other resource routers
router.use('/:postId/comments', commentRouter);
router
.route('/')
.get(getPosts)
.post(protect, createPost);
router
.route('/:id')
.get(getPost)
.put(protect, updatePost)
.delete(protect, deletePost);
module.exports = router;
Step 3.10: Implement Comment Controllers and Routes
File: controllers/commentController.js
const Comment = require('../models/Comment');
const Post = require('../models/Post');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../utils/asyncHandler');
// @desc Get comments
// @route GET /api/comments
// @route GET /api/posts/:postId/comments
// @access Public
exports.getComments = asyncHandler(async (req, res, next) => {
if (req.params.postId) {
const comments = await Comment.find({ post: req.params.postId })
.populate({
path: 'author',
select: 'name username'
});
return res.status(200).json({
success: true,
count: comments.length,
data: comments
});
} else {
res.status(200).json(res.advancedResults);
}
});
// @desc Get single comment
// @route GET /api/comments/:id
// @access Public
exports.getComment = asyncHandler(async (req, res, next) => {
const comment = await Comment.findById(req.params.id)
.populate({
path: 'post',
select: 'title'
})
.populate({
path: 'author',
select: 'name username'
});
if (!comment) {
return next(
new ErrorResponse(`Comment not found with id of ${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: comment
});
});
// @desc Add comment
// @route POST /api/posts/:postId/comments
// @access Private
exports.addComment = asyncHandler(async (req, res, next) => {
req.body.post = req.params.postId;
req.body.author = req.user.id;
const post = await Post.findById(req.params.postId);
if (!post) {
return next(
new ErrorResponse(`Post not found with id of ${req.params.postId}`, 404)
);
}
const comment = await Comment.create(req.body);
res.status(201).json({
success: true,
data: comment
});
});
// @desc Update comment
// @route PUT /api/comments/:id
// @access Private
exports.updateComment = asyncHandler(async (req, res, next) => {
let comment = await Comment.findById(req.params.id);
if (!comment) {
return next(
new ErrorResponse(`Comment not found with id of ${req.params.id}`, 404)
);
}
// Make sure user is comment author or admin
if (
comment.author.toString() !== req.user.id &&
req.user.role !== 'admin'
) {
return next(
new ErrorResponse(
`User ${req.user.id} is not authorized to update this comment`,
401
)
);
}
comment = await Comment.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
res.status(200).json({
success: true,
data: comment
});
});
// @desc Delete comment
// @route DELETE /api/comments/:id
// @access Private
exports.deleteComment = asyncHandler(async (req, res, next) => {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return next(
new ErrorResponse(`Comment not found with id of ${req.params.id}`, 404)
);
}
// Make sure user is comment author or admin
if (
comment.author.toString() !== req.user.id &&
req.user.role !== 'admin'
) {
return next(
new ErrorResponse(
`User ${req.user.id} is not authorized to delete this comment`,
401
)
);
}
await comment.remove();
res.status(200).json({
success: true,
data: {}
});
});
File: routes/comments.js
const express = require('express');
const {
getComments,
getComment,
addComment,
updateComment,
deleteComment
} = require('../controllers/commentController');
const router = express.Router({ mergeParams: true });
const { protect } = require('../middleware/auth');
router
.route('/')
.get(getComments)
.post(protect, addComment);
router
.route('/:id')
.get(getComment)
.put(protect, updateComment)
.delete(protect, deleteComment);
module.exports = router;
Step 3.11: Implement Category Controllers and Routes
File: controllers/categoryController.js
const Category = require('../models/Category');
const ErrorResponse = require('../utils/errorResponse');
const asyncHandler = require('../utils/asyncHandler');
// @desc Get all categories
// @route GET /api/categories
// @access Public
exports.getCategories = asyncHandler(async (req, res, next) => {
res.status(200).json(res.advancedResults);
});
// @desc Get single category
// @route GET /api/categories/:id
// @access Public
exports.getCategory = asyncHandler(async (req, res, next) => {
const category = await Category.findById(req.params.id);
if (!category) {
return next(
new ErrorResponse(`Category not found with id of ${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: category
});
});
// @desc Create new category
// @route POST /api/categories
// @access Private/Admin
exports.createCategory = asyncHandler(async (req, res, next) => {
const category = await Category.create(req.body);
res.status(201).json({
success: true,
data: category
});
});
// @desc Update category
// @route PUT /api/categories/:id
// @access Private/Admin
exports.updateCategory = asyncHandler(async (req, res, next) => {
const category = await Category.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!category) {
return next(
new ErrorResponse(`Category not found with id of ${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: category
});
});
// @desc Delete category
// @route DELETE /api/categories/:id
// @access Private/Admin
exports.deleteCategory = asyncHandler(async (req, res, next) => {
const category = await Category.findById(req.params.id);
if (!category) {
return next(
new ErrorResponse(`Category not found with id of ${req.params.id}`, 404)
);
}
await category.remove();
res.status(200).json({
success: true,
data: {}
});
});
File: routes/categories.js
const express = require('express');
const {
getCategories,
getCategory,
createCategory,
updateCategory,
deleteCategory
} = require('../controllers/categoryController');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
router
.route('/')
.get(getCategories)
.post(protect, authorize('admin'), createCategory);
router
.route('/:id')
.get(getCategory)
.put(protect, authorize('admin'), updateCategory)
.delete(protect, authorize('admin'), deleteCategory);
module.exports = router;
Step 3.12: Implement Advanced Results Middleware
Let's create a middleware for pagination, filtering, and sorting:
File: middleware/advancedResults.js
const advancedResults = (model, populate) => async (req, res, next) => {
let query;
// Copy req.query
const reqQuery = { ...req.query };
// Fields to exclude
const removeFields = ['select', 'sort', 'page', 'limit'];
// Loop over removeFields and delete them from reqQuery
removeFields.forEach(param => delete reqQuery[param]);
// Create query string
let queryStr = JSON.stringify(reqQuery);
// Create operators ($gt, $gte, etc)
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`);
// Finding resource
query = model.find(JSON.parse(queryStr));
// Select Fields
if (req.query.select) {
const fields = req.query.select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (req.query.sort) {
const sortBy = req.query.sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
// Pagination
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 25;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const total = await model.countDocuments();
query = query.skip(startIndex).limit(limit);
if (populate) {
query = query.populate(populate);
}
// Executing query
const results = await query;
// Pagination result
const pagination = {};
if (endIndex < total) {
pagination.next = {
page: page + 1,
limit
};
}
if (startIndex > 0) {
pagination.prev = {
page: page - 1,
limit
};
}
res.advancedResults = {
success: true,
count: results.length,
pagination,
data: results
};
next();
};
module.exports = advancedResults;
Now, let's update our routes to use this middleware:
File: routes/users.js (updated)
const express = require('express');
const {
getUsers,
getUser,
createUser,
updateUser,
deleteUser
} = require('../controllers/userController');
const User = require('../models/User');
const router = express.Router();
const advancedResults = require('../middleware/advancedResults');
const { protect, authorize } = require('../middleware/auth');
router
.route('/')
.get(
protect,
authorize('admin'),
advancedResults(User),
getUsers
)
.post(protect, authorize('admin'), createUser);
router
.route('/:id')
.get(protect, authorize('admin'), getUser)
.put(protect, authorize('admin'), updateUser)
.delete(protect, authorize('admin'), deleteUser);
module.exports = router;
Update other route files similarly to use the advancedResults middleware where appropriate.
Step 4: Review and Extend
Now that we've implemented our core API functionality, let's review what we've built and consider some extensions.
Testing the API
You should test your API with tools like Postman or Insomnia. Create collections for each resource type:
- Authentication (register, login)
- Users (CRUD operations)
- Posts (CRUD operations)
- Comments (CRUD operations)
- Categories (CRUD operations)
Verify that all endpoints work as expected, authentication is functioning, and data is being correctly saved to and retrieved from the database.
Potential Extensions
Here are some ways you could extend this project:
Feature Extensions
- Image Upload: Add support for uploading post featured images
- Tagging System: Implement a system for tagging posts
- Search Functionality: Add full-text search for posts
- Pagination Metadata: Enhance the pagination with more metadata
- User Profiles: Add more user profile functionality
- Password Reset: Implement a password reset flow
- Email Notifications: Send notifications for new comments
Technical Extensions
- Rate Limiting: Add API rate limiting for security
- Caching: Implement response caching for performance
- API Documentation: Generate API docs with Swagger/OpenAPI
- Automated Testing: Write unit and integration tests
- CI/CD Pipeline: Set up continuous integration and deployment
- Containerization: Dockerize the application
Documentation
Documentation is crucial for API usage. Here's a simple template for documenting an endpoint:
# Blog API Documentation
## Authentication
### Register a User
**Endpoint:** `POST /api/auth/register`
**Request Body:**
```json
{
"username": "johndoe",
"email": "john@example.com",
"password": "password123",
"name": "John Doe"
}
```
**Response:**
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### Login
**Endpoint:** `POST /api/auth/login`
**Request Body:**
```json
{
"email": "john@example.com",
"password": "password123"
}
```
**Response:**
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
## Posts
### Get All Posts
**Endpoint:** `GET /api/posts`
**Query Parameters:**
- `page`: Page number (default: 1)
- `limit`: Results per page (default: 25)
- `sort`: Sort field (e.g., sort=createdAt, sort=-title)
- `select`: Fields to select (e.g., select=title,content)
- Other filter parameters (e.g., status=published)
**Response:**
```json
{
"success": true,
"count": 10,
"pagination": {
"next": {
"page": 2,
"limit": 25
}
},
"data": [
{
"_id": "5f7b4b2a6b1a5c0f8c9d6b5a",
"title": "Sample Post",
"content": "This is a sample post",
"author": {
"_id": "5f7b4b2a6b1a5c0f8c9d6b5b",
"name": "John Doe",
"username": "johndoe"
},
"categories": [
{
"_id": "5f7b4b2a6b1a5c0f8c9d6b5c",
"name": "Technology",
"slug": "technology"
}
],
"status": "published",
"createdAt": "2023-05-01T12:00:00.000Z",
"updatedAt": "2023-05-01T12:00:00.000Z"
}
]
}
```
... (continue for all endpoints)
Real-World Deployment Considerations
When deploying your blog API to production, consider the following:
- Environment Variables: Use environment variables for all sensitive information
- Security: Ensure proper CORS settings, rate limiting, and input validation
- Database Choice: Choose between managed database services (MongoDB Atlas, AWS RDS)
- Scalability: Consider how to scale your API if traffic increases
- Monitoring: Implement logging and monitoring to track performance and errors
- Backups: Set up regular database backups
Conclusion and Additional Resources
Congratulations! You've built a comprehensive RESTful API for a blog application with MongoDB integration. This project has combined your knowledge of Node.js, Express, MongoDB, authentication, and API design to create a solid foundation for a full-stack blog application.
Key Takeaways
- Structured API design using the MVC pattern
- MongoDB integration with Mongoose for elegant data modeling
- JWT-based authentication for secure API access
- Advanced features like pagination, filtering, and sorting
- Proper error handling and input validation
- Role-based authorization for different access levels
Additional Resources
Practice Exercises
To solidify your understanding, try implementing these additional features:
Basic Exercises
- Add a tagging system for posts
- Implement a password reset feature
- Add filtering posts by author or category
- Create a simple search functionality for posts
Advanced Exercises
- Implement a full-text search using MongoDB Atlas Search
- Add API rate limiting using express-rate-limit
- Generate API documentation using Swagger
- Add a like/dislike system for posts and comments
- Implement a simple caching mechanism for frequently accessed data