A production-ready RESTful API for managing tasks, built with Node.js, Express.js, and MongoDB following the MVC architecture pattern.
This project is a complete backend solution for a To-Do / Task Management application. It provides a clean REST API with full CRUD operations, input validation, centralised error handling, optional filtering, and a unit test suite β built to production-grade standards.
- β Create, Read, Update, Delete tasks
- β Mark tasks as completed (with double-completion guard)
- β
Filter tasks by
completedstatus orcategory - β
Sort tasks by
createdAtordueDate - β Input validation with meaningful error messages
- β Centralised error handling middleware
- β
Optional
dueDateandcategoryfields - β MVC architecture with clean separation of concerns
- β Async/await throughout β no callback hell
- β Jest + Supertest unit & integration tests
| Layer | Technology |
|---|---|
| Runtime | Node.js |
| Framework | Express.js v4 |
| Database | MongoDB (Mongoose) |
| Environment | dotenv |
| Testing | Jest + Supertest |
| Dev Server | Nodemon |
todo-api/
βββ config/
β βββ db.js # MongoDB connection logic
βββ controllers/
β βββ taskController.js # Business logic for each route
βββ middleware/
β βββ errorMiddleware.js # 404 handler + global error handler
βββ models/
β βββ taskModel.js # Mongoose schema & model
βββ routes/
β βββ taskRoutes.js # Express Router β maps URLs to controllers
βββ tests/
β βββ task.test.js # Jest + Supertest test suite
βββ utils/
β βββ asyncHandler.js # HOF to wrap async controllers
βββ .env # Environment variables (git-ignored)
βββ .env.example # Template for environment variables
βββ .gitignore
βββ package.json
βββ server.js # App entry point
- Node.js v16 or higher
- MongoDB running locally or a MongoDB Atlas cluster
git clone https://github.com/your-username/todo-api.git
cd todo-apinpm installcp .env.example .envOpen .env and fill in your values:
PORT=5000
MONGO_URI=mongodb://localhost:27017/todo_db
NODE_ENV=developmentnpm run devThe server will start at: http://localhost:5000
npm start| Variable | Description | Default |
|---|---|---|
PORT |
Port the server listens on | 5000 |
MONGO_URI |
Full MongoDB connection string | mongodb://localhost:27017/todo_db |
NODE_ENV |
Environment: development/production | development |
| Method | Endpoint | Description |
|---|---|---|
POST |
/tasks |
Create a new task |
GET |
/tasks |
Get all tasks (supports filters) |
GET |
/tasks/:id |
Get a single task by ID |
PUT |
/tasks/:id |
Update a task |
DELETE |
/tasks/:id |
Delete a task |
| Param | Type | Description | Example |
|---|---|---|---|
completed |
Boolean | Filter by completion status | ?completed=true |
category |
String | Filter by category | ?category=work |
sort |
String | Sort by field (prefix - for descending) |
?sort=-createdAt |
Request
POST /api/tasks
Content-Type: application/json
{
"title": "Complete internship assignment",
"description": "Build a Node.js REST API with MVC architecture",
"dueDate": "2025-07-01",
"category": "work"
}Response 201 Created
{
"success": true,
"data": {
"_id": "6651abc123def456789gh012",
"title": "Complete internship assignment",
"description": "Build a Node.js REST API with MVC architecture",
"completed": false,
"dueDate": "2025-07-01T00:00:00.000Z",
"category": "work",
"createdAt": "2025-06-01T10:00:00.000Z",
"updatedAt": "2025-06-01T10:00:00.000Z"
}
}Request
GET /api/tasksResponse 200 OK
{
"success": true,
"count": 2,
"data": [
{
"_id": "6651abc123def456789gh012",
"title": "Complete internship assignment",
"completed": false,
"category": "work",
"createdAt": "2025-06-01T10:00:00.000Z"
},
{
"_id": "6651abc123def456789gh013",
"title": "Buy groceries",
"completed": true,
"category": "personal",
"createdAt": "2025-05-31T08:30:00.000Z"
}
]
}GET /api/tasks?completed=false&category=work&sort=-createdAtRequest
PUT /api/tasks/6651abc123def456789gh012
Content-Type: application/json
{
"completed": true
}Response 200 OK
{
"success": true,
"data": {
"_id": "6651abc123def456789gh012",
"title": "Complete internship assignment",
"completed": true,
"updatedAt": "2025-06-02T14:30:00.000Z"
}
}Request
PUT /api/tasks/6651abc123def456789gh012
Content-Type: application/json
{
"completed": true
}Response 400 Bad Request
{
"success": false,
"message": "Task is already marked as completed"
}Request
DELETE /api/tasks/6651abc123def456789gh012Response 200 OK
{
"success": true,
"message": "Task \"Complete internship assignment\" deleted successfully",
"data": {
"id": "6651abc123def456789gh012"
}
}{
"success": false,
"message": "Task title is required and cannot be empty"
}{
"success": false,
"message": "Task not found with ID: 6651abc123def456789gh012"
}npm testThis runs the full Jest + Supertest test suite with coverage reporting.
For a test-specific MongoDB database, add to .env:
MONGO_URI_TEST=mongodb://localhost:27017/todo_test_db| Layer | File | Responsibility |
|---|---|---|
| Model | models/taskModel.js |
Schema definition, DB interaction |
| View | JSON responses (API) | Structured JSON output |
| Controller | controllers/taskController.js |
Business logic, input validation |
| Router | routes/taskRoutes.js |
Maps HTTP verbs to controller methods |
Without it, an unhandled async error hangs the request indefinitely. This utility wraps every controller so any rejected Promise is automatically forwarded to Express's error pipeline β eliminating repetitive try/catch blocks.
All error formatting lives in one place. Controllers simply throw or set res.status() and throw. This ensures 100% consistent error response shapes across the entire API.
By default, Mongoose skips schema validators on findByIdAndUpdate. Enabling this ensures constraints like maxlength and enum are enforced on edits, not just on creation.
- JWT authentication & user accounts
- Pagination for large task lists (
?page=1&limit=10) - Task priority levels (low / medium / high)
- Search tasks by keyword in title/description
- Rate limiting (express-rate-limit)
- Swagger/OpenAPI documentation
- Docker containerisation
- CI/CD pipeline with GitHub Actions
- In-memory MongoDB (mongodb-memory-server) for isolated testing
MIT Β© 2025