This project is a backend API for a Notes App built using Node.js, Express.js, MongoDB, and Mongoose.
It includes user authentication, notes CRUD, owner-based access control, soft delete, archive functionality, MongoDB aggregation analytics, and full-text search API.
This backend contains 3 main projects:
- User and notes collections
- Each note is connected to a user using reference
- CRUD operations for notes
- Only the note owner can access their notes
- Mongoose populate to fetch user details with notes
- List all notes of logged-in user
- Get a single note
- Soft delete and archive option for notes
- Uses MongoDB aggregation pipeline
- Calculates note summaries
- Counts total notes, archived notes, and active notes
- Counts notes by category
- Shows notes created per month
- Supports filtering by user, category, and date range
- Uses
$match,$group,$project, and$sort
- Uses MongoDB full-text search
- Searches notes by title, content, category, and tags
- Creates text index on searchable fields
- Provides
/api/searchendpoint - Returns matching documents sorted by relevance
- Supports filters like category, tag, and date range
- Node.js
- Express.js
- MongoDB
- Mongoose
- JWT
- bcryptjs
- dotenv
- Nodemon
notes-app-backend/
│
├── config/
│ └── db.js
│
├── controllers/
│ ├── authController.js
│ ├── noteController.js
│ ├── analyticsController.js
│ └── searchController.js
│
├── middleware/
│ └── authMiddleware.js
│
├── models/
│ ├── User.js
│ └── Note.js
│
├── routes/
│ ├── authRoutes.js
│ ├── noteRoutes.js
│ ├── analyticsRoutes.js
│ └── searchRoutes.js
│
├── .env
├── server.js
├── package.json
└── README.mdgit clone https://github.com/your-username/notes-app-backend.gitcd notes-app-backendnpm installCreate a .env file in the root folder and add the following:
PORT=5000
MONGO_URI=mongodb://127.0.0.1:27017/notes_app
JWT_SECRET=your_secret_keyFor development:
npm run devFor production:
npm startServer will run on:
http://localhost:5000| Variable | Description |
|---|---|
| PORT | Server running port |
| MONGO_URI | MongoDB connection URL |
| JWT_SECRET | Secret key for JWT token |
This project uses JWT authentication.
After login, copy the token from the response and send it in protected routes using the Authorization header:
Authorization: Bearer your_jwt_tokenPOST /api/auth/registerRequest Body:
{
"name": "John",
"email": "john@example.com",
"password": "123456"
}Success Response:
{
"success": true,
"message": "User registered successfully",
"token": "jwt_token_here",
"user": {
"id": "user_id",
"name": "John",
"email": "john@example.com"
}
}POST /api/auth/loginRequest Body:
{
"email": "john@example.com",
"password": "123456"
}Success Response:
{
"success": true,
"message": "Login successful",
"token": "jwt_token_here",
"user": {
"id": "user_id",
"name": "John",
"email": "john@example.com"
}
}All notes routes are protected.
POST /api/notesRequest Body:
{
"title": "MongoDB Text Search",
"content": "Learning full-text search in MongoDB",
"category": "Database",
"tags": ["mongodb", "search", "backend"]
}Success Response:
{
"success": true,
"message": "Note created successfully",
"note": {
"_id": "note_id",
"title": "MongoDB Text Search",
"content": "Learning full-text search in MongoDB",
"category": "Database",
"tags": ["mongodb", "search", "backend"],
"user": "user_id",
"isArchived": false,
"isDeleted": false
}
}GET /api/notesSuccess Response:
{
"success": true,
"count": 1,
"notes": [
{
"_id": "note_id",
"title": "MongoDB Text Search",
"content": "Learning full-text search in MongoDB",
"category": "Database",
"tags": ["mongodb", "search"],
"user": {
"_id": "user_id",
"name": "John",
"email": "john@example.com"
},
"isArchived": false,
"isDeleted": false
}
]
}GET /api/notes/:idExample:
GET /api/notes/65f123abc456def789000111PUT /api/notes/:idRequest Body:
{
"title": "Updated Note",
"content": "This note has been updated",
"category": "Backend",
"tags": ["node", "express"]
}PATCH /api/notes/:id/archiveThis endpoint changes the note archive status.
If the note is not archived, it will archive it.
If the note is already archived, it will unarchive it.
Success Response:
{
"success": true,
"message": "Note archived successfully",
"note": {
"_id": "note_id",
"title": "MongoDB Text Search",
"isArchived": true,
"isDeleted": false
}
}GET /api/notes/archivedGET /api/notes/user-with-notesSuccess Response:
{
"success": true,
"user": {
"_id": "user_id",
"name": "John",
"email": "john@example.com"
},
"notes": [
{
"_id": "note_id",
"title": "MongoDB Text Search",
"content": "Learning full-text search in MongoDB",
"user": "user_id",
"isArchived": false,
"isDeleted": false
}
]
}DELETE /api/notes/:idSuccess Response:
{
"success": true,
"message": "Note deleted successfully"
}This endpoint does not permanently delete the note from MongoDB.
It only updates the note like this:
{
"isDeleted": true
}| Method | Endpoint | Description |
|---|---|---|
| POST | /api/notes |
Create a new note |
| GET | /api/notes |
Get all notes of logged-in user |
| GET | /api/notes/:id |
Get a single note |
| PUT | /api/notes/:id |
Update a note |
| DELETE | /api/notes/:id |
Soft delete a note |
| PATCH | /api/notes/:id/archive |
Archive or unarchive note |
| GET | /api/notes/archived |
Get archived notes |
| GET | /api/notes/user-with-notes |
Get user with notes |
The analytics API uses MongoDB aggregation pipeline to calculate summaries and grouped results.
All analytics routes are protected.
GET /api/analytics/summaryExample Response:
{
"success": true,
"filters": {
"category": "All",
"startDate": null,
"endDate": null
},
"analytics": {
"totalNotes": 5,
"archivedNotes": 2,
"activeNotes": 3
}
}GET /api/analytics/summary?category=DatabaseGET /api/analytics/summary?startDate=2026-05-01&endDate=2026-05-31GET /api/analytics/notes-by-categoryExample Response:
{
"success": true,
"count": 3,
"data": [
{
"totalNotes": 4,
"category": "Database"
},
{
"totalNotes": 2,
"category": "Backend"
},
{
"totalNotes": 1,
"category": "General"
}
]
}GET /api/analytics/notes-by-category?startDate=2026-05-01&endDate=2026-05-31GET /api/analytics/notes-by-monthExample Response:
{
"success": true,
"count": 2,
"data": [
{
"totalNotes": 3,
"year": 2026,
"month": 4
},
{
"totalNotes": 5,
"year": 2026,
"month": 5
}
]
}| Method | Endpoint | Description |
|---|---|---|
| GET | /api/analytics/summary |
Get total, active, and archived notes |
| GET | /api/analytics/summary?category=Database |
Get summary by category |
| GET | /api/analytics/summary?startDate=2026-05-01&endDate=2026-05-31 |
Get summary by date range |
| GET | /api/analytics/notes-by-category |
Get notes count per category |
| GET | /api/analytics/notes-by-category?startDate=2026-05-01&endDate=2026-05-31 |
Get category count by date range |
| GET | /api/analytics/notes-by-month |
Get notes created per month |
Used to filter documents before grouping.
{
$match: {
user: userId,
isDeleted: false
}
}Used to group documents and calculate totals.
{
$group: {
_id: "$category",
totalNotes: { $sum: 1 }
}
}Used to format the final output.
{
$project: {
_id: 0,
category: "$_id",
totalNotes: 1
}
}Used to sort the result.
{
$sort: {
totalNotes: -1
}
}The search API allows users to search notes by text.
It uses MongoDB full-text search with $text.
All search routes are protected.
The following fields are searchable:
- title
- content
- category
- tags
A text index is created on these fields:
noteSchema.index({
title: "text",
content: "text",
category: "text",
tags: "text"
});GET /api/search?q=mongodbFull URL:
http://localhost:5000/api/search?q=mongodbExample Response:
{
"success": true,
"query": "mongodb",
"count": 2,
"notes": [
{
"_id": "note_id",
"title": "MongoDB Text Search",
"content": "Using text index and text search in MongoDB",
"category": "Database",
"tags": ["mongodb", "search"],
"user": {
"_id": "user_id",
"name": "John",
"email": "john@example.com"
},
"isArchived": false,
"isDeleted": false,
"score": 1.5
}
]
}GET /api/search?q=mongodb&category=DatabaseGET /api/search?q=mongodb&tag=searchGET /api/search?q=mongodb&startDate=2026-05-01&endDate=2026-05-31GET /api/search?q=mongodb&category=Database&tag=search&startDate=2026-05-01&endDate=2026-05-31| Method | Endpoint | Description |
|---|---|---|
| GET | /api/search?q=mongodb |
Search notes by keyword |
| GET | /api/search?q=mongodb&category=Database |
Search with category filter |
| GET | /api/search?q=mongodb&tag=search |
Search with tag filter |
| GET | /api/search?q=mongodb&startDate=2026-05-01&endDate=2026-05-31 |
Search with date range |
| GET | /api/search?q=mongodb&category=Database&tag=search |
Search with multiple filters |
A text index allows MongoDB to search string content efficiently.
noteSchema.index({
title: "text",
content: "text",
category: "text",
tags: "text"
});The $text operator searches inside the indexed fields.
{
$text: {
$search: q
}
}MongoDB returns a relevance score for matching documents.
{
score: {
$meta: "textScore"
}
}Results are sorted by the best matching notes first.
.sort({
score: {
$meta: "textScore"
}
})const mongoose = require("mongoose");
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true
},
email: {
type: String,
required: [true, "Email is required"],
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: [true, "Password is required"]
}
},
{
timestamps: true
}
);
module.exports = mongoose.model("User", userSchema);const mongoose = require("mongoose");
const noteSchema = new mongoose.Schema(
{
title: {
type: String,
required: [true, "Title is required"],
trim: true
},
content: {
type: String,
required: [true, "Content is required"]
},
category: {
type: String,
default: "General",
trim: true
},
tags: [
{
type: String,
trim: true
}
],
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
},
isArchived: {
type: Boolean,
default: false
},
isDeleted: {
type: Boolean,
default: false
}
},
{
timestamps: true
}
);
noteSchema.index({
title: "text",
content: "text",
category: "text",
tags: "text"
});
module.exports = mongoose.model("Note", noteSchema);Each note belongs to one user using MongoDB reference.
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
}When fetching, updating, deleting, archiving, searching, or analysing notes, the API checks the logged-in user ID.
Example:
{
_id: req.params.id,
user: req.user._id,
isDeleted: false
}This ensures one user cannot access another user’s notes.
This project uses populate() to fetch user details with notes.
Example:
const notes = await Note.find({
user: req.user._id,
isDeleted: false
}).populate("user", "name email");Instead of permanently deleting notes, the project uses soft delete.
The note model contains:
isDeleted: {
type: Boolean,
default: false
}When a note is deleted:
isDeleted: trueNormal note listing only shows:
isDeleted: falseThe note model contains:
isArchived: {
type: Boolean,
default: false
}A note can be archived or unarchived using:
PATCH /api/notes/:id/archivePOST http://localhost:5000/api/auth/registerBody:
{
"name": "John",
"email": "john@example.com",
"password": "123456"
}POST http://localhost:5000/api/auth/loginBody:
{
"email": "john@example.com",
"password": "123456"
}Copy the token from the response.
In Postman, go to Login request:
Scripts > Post-response
Paste this script:
const response = pm.response.json();
if (response.token) {
pm.environment.set("auth_token", response.token);
console.log("Token saved successfully:", response.token);
} else {
console.log("Token not found in response");
}In protected requests, go to Authorization tab:
Type: Bearer Token
Token: {{auth_token}}
Or add it manually in Headers:
Authorization: Bearer {{auth_token}}POST http://localhost:5000/api/notesBody:
{
"title": "MongoDB Aggregation",
"content": "Learning aggregation pipeline with group and project stages",
"category": "Database",
"tags": ["mongodb", "analytics"]
}Another sample:
{
"title": "Express Authentication",
"content": "Learning JWT authentication and protected routes",
"category": "Backend",
"tags": ["express", "jwt", "auth"]
}Another sample:
{
"title": "MongoDB Text Search",
"content": "Using text index and text search in MongoDB",
"category": "Database",
"tags": ["mongodb", "search"]
}| Status Code | Meaning |
|---|---|
| 200 | Request successful |
| 201 | Created successfully |
| 400 | Bad request or missing fields |
| 401 | Unauthorized or invalid token |
| 404 | Data not found |
| 409 | User already exists |
| 500 | Server error |
{
"success": false,
"message": "Note not found or access denied"
}- Add pagination for notes
- Add restore deleted note feature
- Add permanent delete option
- Add note priority
- Add reminder date for notes
- Add refresh token support
- Add regex-based search fallback
- Add analytics charts on frontend
- Add export analytics report
- Add multiple tag filtering
- Add role-based access control
Vikas Kushwaha
This project is open-source and created for learning and practice purposes.