A full-featured blog application built with MongoDB, Express, React, and Node.js.
- Create, read, and delete blog posts
- Search posts by title or tags (case-insensitive)
- Pagination (5 posts per page) with automatic rollback when a page empties
- Responsive 2026 SaaS-style UI with dark/light mode
- JWT Authentication — signup/login; only the post creator can delete their post
- Likes — like/unlike posts with optimistic UI updates and rollback
- Comments — add/delete comments per post with author + timestamp
- Image Uploads — drag-and-drop image upload (Multer + optional Cloudinary, max 5 MB)
- Dark/Light Mode — toggle with persistence via
localStorage - Toast Notifications — global success/error/info toasts for all user actions
- Rate Limiting — 10 req/15 min on auth, 30/10 min on post creation, 200/min global
- Input Validation — server-side
express-validatoron all mutation endpoints
mini-blog-app/
├── client/ # React frontend (Create React App)
│ ├── src/
│ │ ├── api/axios.js # Axios instance with JWT interceptor
│ │ ├── context/
│ │ │ ├── AuthContext.js # Global JWT auth state (login/signup/logout)
│ │ │ ├── ThemeContext.js # Dark/light theme with localStorage persistence
│ │ │ └── ToastContext.js # Global toast notification system
│ │ ├── components/
│ │ │ ├── Navbar.js # Sticky nav; "New Post" → /?write=true
│ │ │ ├── PostCard.js # Post display with optimistic like/delete + toasts
│ │ │ ├── PostForm.js # Create post with drag-and-drop image upload
│ │ │ ├── CommentSection.js # Per-post comment add/delete section
│ │ │ ├── Pagination.js # Page navigation component
│ │ │ └── Toast.js # Floating toast notification renderer
│ │ └── pages/
│ │ ├── Home.js # Feed: search, URL-param form open, pagination
│ │ ├── Login.js # JWT login form
│ │ └── Signup.js # JWT signup form
│ └── .env # REACT_APP_API_URL, REACT_APP_SERVER_URL
└── server/ # Node/Express backend
├── config/db.js # MongoDB connection via Mongoose
├── controllers/
│ ├── postController.js # CRUD + like + comment handlers
│ └── authController.js # signup, login (JWT)
├── middleware/
│ ├── errorHandler.js # Centralized Express error handler
│ └── authMiddleware.js # protect() — verifies JWT token
├── models/
│ ├── Post.js # Post schema (with text index + indexes)
│ └── User.js # User schema (bcrypt pre-save hook)
├── routes/
│ ├── postRoutes.js # All post routes + Multer upload
│ └── authRoutes.js # /signup, /login
├── uploads/ # Uploaded images (local)
└── server.js # Express entry point
- Node.js v18+
- MongoDB running locally (
mongod) or a MongoDB Atlas connection string
git clone <repo-url>
cd mini-blog-appServer — create/edit server/.env:
PORT=5000
MONGO_URI=mongodb://localhost:27017/mini-blog
JWT_SECRET=replace_with_a_long_random_secret
NODE_ENV=developmentClient — create/edit client/.env:
REACT_APP_API_URL=http://localhost:5000/api
REACT_APP_SERVER_URL=http://localhost:5000Set
NODE_ENV=productionto suppress stack traces in API error responses.
npm run install-allOr manually:
cd server && npm install
cd ../client && npm install# From project root — runs both concurrently
npm run dev
# Or individually:
npm run server # Express API on http://localhost:5000
npm run client # React app on http://localhost:3000Pass JWT token in the Authorization header for protected routes:
Authorization: Bearer <token>
| Method | Endpoint | Body | Description |
|---|---|---|---|
| POST | /api/auth/signup |
{username, email, password} |
Register a new user |
| POST | /api/auth/login |
{email, password} |
Login — returns JWT token |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/posts |
No | Fetch posts (paginated, 5/page) sorted newest first |
| GET | /api/posts?search=term |
No | Search title or tags (case-insensitive) |
| GET | /api/posts?page=2 |
No | Get a specific page |
| POST | /api/posts |
Optional | Create post (multipart/form-data) |
| DELETE | /api/posts/:id |
Required | Delete post — creator only |
| PUT | /api/posts/:id/like |
Required | Toggle like on a post |
| POST | /api/posts/:id/comments |
Required | Add a comment |
| DELETE | /api/posts/:id/comments/:commentId |
Required | Delete own comment |
{
"posts": [...],
"currentPage": 1,
"totalPages": 3,
"total": 15
}{
"title": "String — required, trimmed",
"content": "String — required, trimmed",
"username": "String — required, trimmed",
"tags": ["String"],
"image": "String (URL path, e.g. /uploads/filename.jpg)",
"author": "ObjectId (ref: User) — null if unauthenticated",
"likes": ["ObjectId (ref: User)"],
"comments": [
{
"user": "ObjectId (ref: User)",
"username": "String",
"text": "String",
"createdAt": "Date"
}
],
"createdAt": "Date"
}{
"message": "Human-readable error"
}In development (
NODE_ENV=development) astackfield is also included for debugging.
| # | Item | Notes |
|---|---|---|
| 1 | Set NODE_ENV=production |
Hides stack traces in error responses |
| 2 | Set strong JWT_SECRET (32+ chars) |
Change from the default placeholder |
| 3 | Set CORS_ORIGIN=https://your-domain.com |
Restricts CORS to your frontend URL |
| 4 | Configure Cloudinary env vars | Enables cloud image storage |
| 5 | Use MongoDB Atlas | Replace MONGO_URI with Atlas connection string |
- Images stored locally in
server/uploads/; set Cloudinary env vars to use cloud storage. - JWT tokens expire after 7 days.
- If a post has no
author(created without auth), any logged-in user can delete it. - Pagination is 5 posts per page, configurable via
PAGE_SIZEinpostController.js. - Search input is regex-escaped server-side to prevent ReDoS attacks.
- Login uses constant-time bcrypt compare even for non-existent users to prevent email enumeration.