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:
- Users —
id,name,email(unique),password(hashed), timestamps - Tasks —
id,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
Authorizationheader asBearer TOKEN - The task list must support pagination, filtering, and sorting through query parameters
- Passwords must be hashed with
bcryptbefore 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
| Method | Endpoint | Auth | Request | Success Response |
|---|---|---|---|---|
| POST | /auth/register | Public | Body: name, email, password | 201 — user object without password |
| POST | /auth/login | Public | Body: email, password | 200 — token plus user object |
| GET | /tasks | Private | Query: page, limit, status, priority, sort, search | 200 — paginated array of tasks |
| POST | /tasks | Private | Body: title, description, priority, due_date | 201 — created task |
| GET | /tasks/:id | Private | — | 200 — single task |
| PATCH | /tasks/:id | Private | Body: any updatable task field | 200 — updated task |
| DELETE | /tasks/:id | Private | — | 200 — confirmation message |
The list endpoint response must include pagination metadata:
{
"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:
{
"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
/tasksroutes return401without a valid token - Requesting another user's task by id returns
404, not the task -
POST /tasksvalidates the body with Joi and defaultsstatustotodo -
GET /taskssupportspageandlimitwith sane defaults (page 1, limit 10) -
GET /tasksfilters bystatusandpriority, and searchestitleviasearch -
sortaccepts a field name with an optional-prefix for descending order - Every handler uses
try/catchand errors flow to one central error handler - Environment variables (
PORT,JWT_SECRET, database URL) live in a.envfile - 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:
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:
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 anifafter fetching - Cap
limit(the example caps it at 100) so a client cannot request a million rows - Validate
:idparams too; a malformed id should be400, 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:
- Refresh tokens — short-lived access token plus a refresh endpoint
- Soft delete — a
deleted_atfield instead of removing rows, with a restore endpoint - Due-date queries —
overdue=trueanddue_before=2026-08-01filters - Integration tests — cover the auth flow and tasks CRUD with Jest and Supertest
- Rate limiting — protect
/auth/loginfrom brute force withexpress-rate-limit - Swagger docs — publish the contract at
/api-docsand 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