A simple RESTful backend for a blog application, built with Express.js, MongoDB and JWT-based authentication. Supports CRUD for posts and authors, pagination, validation, error handling — and a secure comment system with ownership/admin permissions and optional nested replies. It also supports flexible search and multi‑criteria filtering for blog posts.
- Features
- Tech Stack
- Prerequisites
- Project Structure
- Setup & Installation
- Environment Variables
- Database
- Running the Server
- Authentication
- API Endpoints
- Validation & Error Handling
- Pagination
- Notes on Comments & Permissions
- Search & Filtering Implementation Notes
- Further Improvements
- User Authentication via JSON Web Tokens (JWT)
- CRUD operations for blog posts
- Author registration & lookup (passwords excluded from responses)
- Pagination support on posts listing
- Secure Comment System: create/read/update/delete comments tied to posts, owner/admin restrictions, optional nested replies
- Flexible Search & Multi‑criteria Filtering for posts (search by title/content/tags, date range, category/tags, sorting)
- Input Validation using
express-validator - Centralized Error Handling via middleware
- Protected Routes: only authenticated authors can create/update/delete their own posts and comments (unless admin)
- Node.js & Express.js
- MongoDB with Mongoose ODM
- JWT for token-based auth
- bcrypt.js for password hashing
- dotenv for environment config
- express-validator for request validation
- Node.js v14+
- MongoDB v4+ (local or Atlas)
- npm (or yarn)
blog-api/
├── .env
├── package.json
├── index.js
├── config/
│ └── db.js
├── models/
│ ├── Author.js # includes `role` (author | admin)
│ ├── Post.js # includes category, tags, indexes
│ └── Comment.js # comments model (post, author, content, parent, timestamps)
├── routes/
│ ├── auth.js
│ ├── authors.js
│ ├── posts.js
│ ├── comments.js
└── middleware/
├── auth.js
└── errorHandler.js
-
Clone the repo
git clone https://github.com/ali-amir-code/t8-advanced-search.git cd t8-advanced-search -
Install dependencies
npm install
-
Create
.env(see next) -
Ensure
server.jsregisters routes (example):app.use('/auth', require('./routes/auth')); app.use('/authors', require('./routes/authors')); app.use('/posts', require('./routes/posts')); app.use('/', require('./routes/comments')); // or app.use('/comments', require('./routes/comments')) app.use('/', require('./routes/blogs')); // search/filtering endpoint (optional)
Create a .env file in the project root with:
PORT=5000
MONGO_URI=mongodb://localhost:27017/blogdb
JWT_SECRET=your_jwt_secret_herePORT— Server portMONGO_URI— MongoDB connection stringJWT_SECRET— Secret key for signing JWT tokens
-
Default database:
blogdbon localhost. -
Collections:
authors(fields:_id,name,email,password,role) —roleisauthororadmin.posts(fields:_id,title,content,author(ref),category,tags,createdAt)comments(fields:_id,post(ref),author(ref),content,parent(ref|null),createdAt,updatedAt)
-
Development (with auto-reload):
npm run dev
-
Production:
npm start
Server will be listening on http://localhost:<PORT>.
All protected routes require an Authorization header:
Authorization: Bearer <token>
- Register:
POST /auth/register→ returns{ token }
Token payload includes{ author: { id, role } }so middleware can check role (admin/author). - Login:
POST /auth/login→ returns{ token }
| Method | Endpoint | Body | Response |
|---|---|---|---|
| POST | /auth/register |
{ name, email, password (min 6 chars) } |
{ token } |
| POST | /auth/login |
{ email, password } |
{ token } |
Protected: require
Authorizationheader.
| Method | Endpoint | Response |
|---|---|---|
| GET | /authors |
[{ _id, name, email, role }] |
| GET | /authors/:id |
{ _id, name, email, role } |
Public read; protected write/update/delete.
| Method | Endpoint | Query / Body | Response |
|---|---|---|---|
| GET | /posts |
?limit=<n>&page=<p> (optional, defaults: limit=10, page=1) |
[{ _id, title, content, author:{_id,name}, createdAt }] |
| GET | /posts/:id |
— | { _id, title, content, author:{_id,name}, createdAt } |
| POST | /posts |
{ title, content, category?, tags? } (protected) |
201 Created + newly created post object |
| PUT | /posts/:id |
{ title? (optional), content? (optional), category?, tags? } |
Updated post object |
| DELETE | /posts/:id |
— (protected, owner only) | { msg: "Post removed" } |
Protected for create/update/delete. Read is public. Only the comment owner or an admin may update/delete a comment. Supports optional nested replies via a
parentfield.
-
POST
/posts/:id/comments
Add a comment to a post. Body:{ content: string, parent?: commentId | null }
Validates non-emptycontent. Ifparentprovided, parent must exist and belong to same post. -
GET
/posts/:id/comments
Fetch all comments for a post. Returns a flat list or a nested tree (each comment includeschildren: []array). Sort order: ascending bycreatedAtby default. -
PUT
/comments/:id
Update a comment. Body:{ content: string }
Only owner or admin allowed. -
DELETE
/comments/:id
Delete a comment. Only owner or admin allowed. (Note: cascade / re-parent behavior configurable.)
- Missing fields: returns
400 Bad Request+ array of validation errors (e.g., empty comment content is rejected). - Unauthorized: returns
401 Unauthorizedwhen token missing/invalid. - Forbidden: returns
403 Forbiddenwhen a non-owner/non-admin attempts edit/delete. - Not found: returns
404 Not Foundfor missing posts/authors/comments. - Server errors:
500 Internal Server Errorwith{ error }.
All errors funnel through middleware/errorHandler.js for consistent JSON responses.
- Clients may request page-by-page data via query parameters:
GET /posts?limit=5&page=2- Response returns up to
limitposts, skipping(page-1)*limit.
Comment lists may also be paginated in future (not implemented by default).
- Ownership: Comments store
author(ObjectId). Only the comment owner (matchingreq.author.id) or a user withrole === 'admin'may update or delete a comment. - Nested replies: Implemented with an optional
parentfield (ObjectId referencing another Comment).GET /posts/:id/commentsreturns achildrentree for nested replies. - Delete behavior: By default, deleting a comment removes that single comment. You can choose to implement cascade delete (recursively remove children), re-parent children, or soft-delete (mark as deleted while keeping children).
- Token payload: Ensure JWT payload includes role, e.g.:
const payload = { author: { id: author.id, role: author.role } };So middleware/auth.js can expose req.author.id and req.author.role.
This project includes a flexible search/filter endpoint to let end-users query posts by keyword, tags, category, date range, sorting and pagination.
Endpoint (example)
GET /blogs?search=keyword&category=tech&tags=nodejs,express&fromDate=2024-01-01&toDate=2024-12-31&sort=latest&page=1&limit=10
Query Parameters
search— keyword to search acrosstitle,content, andtags. Uses case-insensitive regex by default.category— exact match on post category.tags— comma-separated list (e.g.tags=nodejs,express). Matches posts that contain any of the listed tags by default. Usetags_all=trueto require all tags ($all) instead.fromDate— ISO date (YYYY-MM-DD). Inclusive start date forcreatedAt.toDate— ISO date (YYYY-MM-DD). Inclusive end date forcreatedAt.sort—latest(default),oldest,alpha_asc,alpha_desc.page— page number (default1).limit— items per page (default10, max100).
Response (paginated)
{
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5,
"results": [
{
"_id": "60f5a6f3b1a4c730d8f0e8a3",
"title": "New Features in Node.js",
"content": "Node.js v20 introduces …",
"category": "backend",
"tags": ["nodejs","performance"],
"author": { "_id":"60f5a2c4","name":"Alice Smith" },
"createdAt": "2024-09-12T11:00:00.000Z"
}
// ...
]
}- Search: The implementation uses escaped regex for the
searchterm and applies a case‑insensitive regex totitle,content, andtags. This supports partial matches but can be slower than a text index for large datasets. - Indexes: Add indexes on
createdAt,category, andtags(and consider a text index ontitle+contentfor$textqueries) to improve performance. - Validation: Use
express-validatorforfromDate,toDate,page,limit, andsort. Return400 Bad Requestwith helpful error details for invalid parameters. - Date handling:
toDateis treated inclusively (set to end of day if no time provided). - Combining filters: All filters can be combined in a single request. The route builds a MongoDB
filterobject and applies$and/$oras needed. - Performance tips:
- Use
.lean()to reduce Mongoose overhead. - Cap
limitto a reasonable maximum (e.g., 100). - Consider
$textsearch or a dedicated search engine (Meilisearch, Typesense, Elasticsearch) for advanced/fuzzy search. - Cache frequently requested queries (Redis) where appropriate.
- Use
- Advanced options:
tags_all=true— require all tags ($all).fields=title,content— projection to return only selected fields.sort=relevance— if switching to$textsearch.
- Soft deletes for comments (preserve thread context).
- Cascade / re-parent options for replies on delete.
- Comment pagination for posts with many comments.
- Likes/upvotes, moderation tools, and reporting.
- Rate limiting to protect endpoints (e.g., login/comment flood).
- Swagger/OpenAPI docs for automatic API documentation.
- Unit & integration tests (Jest, Supertest).
- Dockerization & CI/CD for production deployment.