This article was written by Toheeb Abdulsalam.
Why build a Task Manager API?
A Task Manager API is a compact, real-world backend that teaches routing, request handling, data modeling, and database integration. It’s perfect for beginners because it’s small enough to finish quickly yet demonstrates important backend concepts you’ll reuse in larger projects.
What you’ll need (Prerequisites)
- Node.js (v14+ recommended) and npm installed
- MongoDB running locally or a MongoDB Atlas connection string
- Basic command line familiarity
- Postman, Insomnia, or curl for testing
Project setup
Open a terminal and run:
mkdir task-manager-api
cd task-manager-api
npm init -ynpm install express mongoose cors dotenvnpm install --save-dev nodemonAdd scripts to package.json:
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}Create this basic file structure:
task-manager-api/
├─ index.js
├─ .env
├─ models/
│ └─ Task.js
├─ routes/
│ └─ tasks.js
└─ package.jsonCreate a .env file:
PORT=3000
MONGO_URI=mongodb://127.0.0.1:27017/taskdbIf you use MongoDB Atlas, replace the MONGO_URI value with your Atlas connection string.
index.js — create the server & connect to MongoDB
Create index.js and paste the following:
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
app.use(express.json());
app.use(cors());
const PORT = process.env.PORT || 3000;
const MONGO_URI = process.env.MONGO_URI;
mongoose.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => {
console.error(' MongoDB connection error:', err.message);
process.exit(1);
});
app.get('/', (req, res) => res.json({ message: 'Task Manager API' }));
const tasksRouter = require('./routes/tasks');
app.use('/api/tasks', tasksRouter);
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));Explanation:
dotenv loads .env vars.
express.json() parses JSON body payloads.
cors() allows cross-origin requests (handy while developing a frontend).
We mount task routes under /api/tasks.
models/Task.js — define the Task schema
Create models/Task.js:
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
description: { type: String, default: '', trim: true },
completed: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Task', taskSchema);Why Mongoose?
Mongoose gives a simple schema layer over MongoDB, validation, and helpful methods for queries.
routes/tasks.js — implement full CRUD
Create routes/tasks.js:
const express = require('express');
const router = express.Router();
const Task = require('../models/Task');
// Create a new task
router.post('/', async (req, res) => {
try {
const { title, description } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const task = new Task({ title, description });
await task.save();
return res.status(201).json(task);
} catch (err) {
return res.status(500).json({ error: 'Server error', details: err.message });
}
});
// Get all tasks
router.get('/', async (req, res) => {
try {
const tasks = await Task.find().sort({ createdAt: -1 });
return res.json(tasks);
} catch (err) {
return res.status(500).json({ error: 'Server error', details: err.message });
}
});
// Get a single task
router.get('/:id', async (req, res) => {
try {
const task = await Task.findById(req.params.id);
if (!task) return res.status(404).json({ error: 'Task not found' });
return res.json(task);
} catch (err) {
return res.status(500).json({ error: 'Server error', details: err.message });
}
});
// Update a task
router.put('/:id', async (req, res) => {
try {
const { title, description, completed } = req.body;
const update = { title, description, completed };
const task = await Task.findByIdAndUpdate(
req.params.id,
update,
{ new: true, runValidators: true }
);
if (!task) return res.status(404).json({ error: 'Task not found' });
return res.json(task);
} catch (err) {
return res.status(500).json({ error: 'Server error', details: err.message });
}
});
// Delete a task
router.delete('/:id', async (req, res) => {
try {
const task = await Task.findByIdAndDelete(req.params.id);
if (!task) return res.status(404).json({ error: 'Task not found' });
return res.json({ message: 'Task deleted' });
} catch (err) {
return res.status(500).json({ error: 'Server error', details: err.message });
}
});
module.exports = router;Key points:
- Each route uses async/await – Mongoose operations are asynchronous and return Promises. Using async/await keeps the route logic clean, readable, and easy to manage while providing simple try/catch error handling and ensuring the server remains non-blocking and fast.
- Basic validation (title required).
- findByIdAndUpdate uses { new: true } to return the updated document.
- Using { new: true } ensures your API always returns the updated version of the document, which leads to cleaner logic and more reliable frontend–backend communication.
Let’s run the app!
Start the server:
npm run devYou should see: Connected to MongoDB and Server running on port 3000.
Testing the API (curl examples)
Create a task:
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Buy groceries","description":"Milk, bread, eggs"}'List all tasks:
curl http://localhost:3000/api/tasksGet one task:
curl http://localhost:3000/api/tasks/<TASK_ID>Update a task:

Delete a task:

Or use Postman/Insomnia to visually test the endpoints.
Common issues & fixes
- MongoDB connection error:
Check .env MONGO_URI value.
If using Atlas, whitelist your IP or enable access from anywhere (temporary, for testing).
Ensure MongoDB server is running locally (mongodb).
- CORS errors from frontend:
Cross-Origin Resources Sharing (CORS) is a web security feature used by browsers that allows a website to make requests to another website.
app.use(cors()) allows all websites.
For production lock to your domain:
app.use(cors({ origin: ‘https://yourfrontend.com’ }))
- Validation error:
Mongoose returns validation errors — format them for user-friendly messages before returning in production.
- 404 when fetching by ID:
Ensure ID is a valid MongoDB ObjectId. If it’s invalid, findById throws — catch and return a 400.
Security & performance tips
- Environment variables: never commit .env. Use secure secrets management in production.
- Rate limiting: add express-rate-limit to prevent abuse.
Rate limiting prevents users from sending too many requests too quickly, protecting your API from abuse and overload.
- Input validation: use Joi or express-validator for robust validation.
Input validation ensures that users send correct, safe data to your API. It prevents malformed input, protects your database, and improves security.
Joi is a data validation library for Nodejs that allows you to define schemas and validate data structures against the schema that you defined, making it flexible and reusable.
Express-validator is a set of middleware functions for validating and sanitizing request data directly inside Express routes.
- Index fields: if you query by fields other than _id, create indexes to improve performance.
- Pagination: use limit and skip or cursor-based pagination for large datasets.
How to extend this project (next steps)
- Add authentication (JWT): create User model, protect routes so users manage only their tasks.
- Attach userId to tasks: ensure tasks belong to users.
- Pagination & filtering: support ?completed=true and paging query params.
- File uploads: allow attaching images to tasks (store on S3 or Cloudinary).
- Rate limit & logging: add request logging (Winston) and monitoring.
- Testing: add unit & integration tests (Jest + Supertest).
Wrapping up
You now have a fully functional Task Manager API with Express and MongoDB — complete with CRUD operations, error handling, and a clean project structure you can build on. This is the kind of foundation that scales: add authentication, pagination, or a frontend, and you’ve got a production-ready application.
When you’re ready to deploy, Galaxy makes it simple. Push your Node.js app and connect to a managed MongoDB database — no DevOps headaches, no complex configurations. Galaxy handles the infrastructure so you can focus on building.