Skip to main content

Task Manager API

You are tasked with building the backend for a Task Manager application. Every user registers, logs in, and manages their own private list of tasks. This project pulls together everything from the Express section: routing, middleware, JWT authentication, validation, pagination, and consistent error handling.

What to Build

There are two entities:

  1. Usersid, name, email (unique), password (hashed), timestamps
  2. Tasksid, user_id, title, description, status (todo, in_progress, done), priority (low, medium, high), due_date, timestamps

Rules of the game:

  • Every task belongs to exactly one user, and a user can never read, update, or delete another user's tasks
  • All task routes are private — they require a valid JWT in the Authorization header as Bearer TOKEN
  • The task list must support pagination, filtering, and sorting through query parameters
  • Passwords must be hashed with bcrypt before saving — never store plain text

Recommended stack: Node.js, Express.js, MongoDB with Mongoose (or PostgreSQL if you prefer), Joi for validation, jsonwebtoken for auth, and Postman for testing.

API Contract

MethodEndpointAuthRequestSuccess Response
POST/auth/registerPublicBody: name, email, password201 — user object without password
POST/auth/loginPublicBody: email, password200token plus user object
GET/tasksPrivateQuery: page, limit, status, priority, sort, search200 — paginated array of tasks
POST/tasksPrivateBody: title, description, priority, due_date201 — created task
GET/tasks/:idPrivate200 — single task
PATCH/tasks/:idPrivateBody: any updatable task field200 — updated task
DELETE/tasks/:idPrivate200 — confirmation message

The list endpoint response must include pagination metadata:

Response of GET /tasks?page=1&limit=10&status=todo&sort=-due_date
{
"status": 200,
"response": "OK",
"message": "Tasks fetched successfully",
"data": [
{
"id": "60f1c7b0e9b5a8a2b4b0b5a1",
"title": "Write project readme",
"description": "Cover setup, env vars, and API docs",
"status": "todo",
"priority": "high",
"due_date": "2026-08-01T00:00:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 10,
"total_items": 42,
"total_pages": 5
}
}

Error responses follow one shape everywhere:

Response of a validation failure
{
"status": 400,
"response": "Bad Request",
"message": "\"title\" is required"
}

Use 400 for invalid input, 401 for missing or invalid tokens, 403 for tokens that are valid but not allowed to touch the resource, 404 for unknown ids, and 500 for unexpected server errors.

Requirements Checklist

  • Registration hashes the password and rejects duplicate emails with 400
  • Login returns a signed JWT that expires (for example in 1 day)
  • An auth middleware verifies the token and attaches the decoded user to the request
  • All /tasks routes return 401 without a valid token
  • Requesting another user's task by id returns 404, not the task
  • POST /tasks validates the body with Joi and defaults status to todo
  • GET /tasks supports page and limit with sane defaults (page 1, limit 10)
  • GET /tasks filters by status and priority, and searches title via search
  • sort accepts a field name with an optional - prefix for descending order
  • Every handler uses try/catch and errors flow to one central error handler
  • Environment variables (PORT, JWT_SECRET, database URL) live in a .env file
  • The API is fully tested in Postman with a saved collection

Hints

Start with auth, because everything else depends on it. The middleware is small:

middlewares/auth.js
import jwt from "jsonwebtoken";

export const auth = (req, res, next) => {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;

if (!token) {
return res.status(401).json({
status: 401,
response: "Unauthorized",
message: "Access denied. No token provided.",
});
}

try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
return res.status(401).json({
status: 401,
response: "Unauthorized",
message: "Invalid or expired token.",
});
}
};

For the list endpoint, build the filter object from the query string and let the database do the work:

controllers/task.controller.js (list handler sketch)
export const listTasks = async (req, res, next) => {
try {
const page = Math.max(parseInt(req.query.page, 10) || 1, 1);
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 100);

const filter = { user_id: req.user.id };
if (req.query.status) filter.status = req.query.status;
if (req.query.priority) filter.priority = req.query.priority;
if (req.query.search) filter.title = new RegExp(req.query.search, "i");

const sort = req.query.sort || "-createdAt";

const [tasks, total] = await Promise.all([
Task.find(filter).sort(sort).skip((page - 1) * limit).limit(limit),
Task.countDocuments(filter),
]);

res.json({
status: 200,
response: "OK",
message: "Tasks fetched successfully",
data: tasks,
meta: { page, limit, total_items: total, total_pages: Math.ceil(total / limit) },
});
} catch (err) {
next(err);
}
};

More nudges if you get stuck:

  • Scope every query by user_id: req.user.id — ownership checks belong in the query, not in an if after fetching
  • Cap limit (the example caps it at 100) so a client cannot request a million rows
  • Validate :id params too; a malformed id should be 400, not a crash
  • Write the Joi schemas first — they double as documentation of your API contract

Stretch Goals

Once the checklist is green, push further:

  1. Refresh tokens — short-lived access token plus a refresh endpoint
  2. Soft delete — a deleted_at field instead of removing rows, with a restore endpoint
  3. Due-date queriesoverdue=true and due_before=2026-08-01 filters
  4. Integration tests — cover the auth flow and tasks CRUD with Jest and Supertest
  5. Rate limiting — protect /auth/login from brute force with express-rate-limit
  6. Swagger docs — publish the contract at /api-docs and keep it in sync

Evaluation Criteria

  • All endpoints in the contract table work exactly as specified
  • Status codes and response shapes are consistent across every route
  • Users are fully isolated from each other's data
  • Validation errors return helpful messages, not stack traces
  • Code is organized into routes, controllers, models, and middlewares
  • The Postman collection covers happy paths and error cases