Build a Task Manager API in Node.js and MongoDB – CRUD Tutorial

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:

Bash
mkdir task-manager-api
cd task-manager-api
npm init -ynpm install express mongoose cors dotenvnpm install --save-dev nodemon

Add scripts to package.json:

Bash
"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js"
}

Create this basic file structure:

Bash
task-manager-api/
├─ index.js
├─ .env
├─ models/
   └─ Task.js
├─ routes/
   └─ tasks.js
└─ package.json

Create a .env file:

Bash
PORT=3000
MONGO_URI=mongodb://127.0.0.1:27017/taskdb

If 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

Bash
npm run dev

You should see: Connected to MongoDB and Server running on port 3000.

Testing the API (curl examples)

Create a task:

Bash
curl -X POST http://localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Buy groceries","description":"Milk, bread, eggs"}'

List all tasks:

Bash
curl http://localhost:3000/api/tasks

Get one task:

Bash
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

  1. 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).

  1. 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’ }))

  1.  Validation error:

Mongoose returns validation errors — format them for user-friendly messages before returning in production.

  1. 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.