A RESTful backend for a task management system, built with Node.js and Express as part of AWDF Practical 4, then upgraded to store data in MongoDB via Mongoose for Practical 5 (schema design, validation, and real persistence instead of an in-memory array).
You need a MongoDB instance to connect to — pick one:
Option A — local MongoDB. Install MongoDB Community Server,
make sure mongod is running, then the default connection string
(mongodb://127.0.0.1:27017/taskmanager) just works.
Option B — MongoDB Atlas (free, no local install). Create a free cluster at mongodb.com/atlas, add your IP to the access list, and grab the connection string from Database → Connect → Drivers.
Then:
cp .env.example .env
# edit .env and set MONGODB_URI to whichever option you picked
npm install
npm run seed # optional: clears the tasks collection, inserts 4 sample tasks
npm start # node server.js
# or, auto-restart on file changes:
npm run dev # node --watch server.js
Server listens on http://localhost:5000 (override with PORT in .env).
On startup it connects to MongoDB first — if the connection fails, it logs
the error and exits instead of serving an API that would 500 on every request.
models/Task.js defines the Mongoose schema:
| Field | Type | Rules |
|---|---|---|
title |
String | required, trimmed, 1-120 chars |
description |
String | optional, trimmed, up to 1000 chars, defaults to '' |
completed |
Boolean | defaults to false |
createdAt, updatedAt |
Date | added automatically (timestamps: true) |
_id |
ObjectId | Mongo's default primary key — ids are now 24-char hex strings, not sequential integers like Practical 4 |
Validation lives on the schema, not hand-rolled in the controller — it's
enforced no matter what calls .save()/.create() (controller, seed
script, or anything else that touches the model), and runValidators: true
on updates makes sure partial PUT requests are checked too.
Client (Postman/browser)
│
▼
express.json() parses JSON bodies
│
▼
requestLogger logs "METHOD URL - ISO timestamp" for every request
│
▼
Express Router (/tasks)
├── requireJsonContentType POST/PUT only — 415 if Content-Type isn't application/json
├── validateTaskId :id routes only — 400 if the id isn't a valid Mongo ObjectId
└── controller (CRUD) talks to MongoDB via the Task model
│
▼
notFoundHandler catches any request that matched nothing above — 404 JSON
│
▼
errorHandler LAST middleware — catches every next(err):
• Mongoose ValidationError → 400
• Mongoose CastError (bad id reaching a query) → 400
• everything else → err.status or 500
The error handler is registered after every route on purpose: Express only
routes an error to a handler that comes after the point next(err) was
called, so if it were registered first it would never see anything.
| Method | Path | Status codes | Description |
|---|---|---|---|
| GET | /tasks |
200 | List all tasks |
| GET | /tasks/:id |
200, 400, 404 | Get one task |
| POST | /tasks |
201, 400, 415 | Create a task (title required) |
| PUT | /tasks/:id |
200, 400, 404, 415 | Update a task (partial body allowed) |
| DELETE | /tasks/:id |
200, 400, 404 | Delete a task |
Task shape (as returned by the API):
{
"_id": "665f1a2b3c4d5e6f7a8b9c0d",
"title": "string",
"description": "string",
"completed": false,
"createdAt": "2026-08-13T12:00:00.000Z",
"updatedAt": "2026-08-13T12:00:00.000Z",
"__v": 0
}Run npm run seed first if you want existing tasks to GET/PUT/DELETE
against — copy an _id from its output, since ids are generated by Mongo
(you can't hardcode 1/2/3 like Practical 4 anymore).
# list
curl http://localhost:5000/tasks
# create
curl -X POST http://localhost:5000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Write the README","description":"Document the API"}'
# get one (swap in a real _id from the response above)
curl http://localhost:5000/tasks/<id>
# update
curl -X PUT http://localhost:5000/tasks/<id> \
-H "Content-Type: application/json" \
-d '{"completed":true}'
# delete
curl -X DELETE http://localhost:5000/tasks/<id>
# 400 — missing title (schema validation)
curl -X POST http://localhost:5000/tasks \
-H "Content-Type: application/json" -d '{"description":"no title"}'
# 400 — malformed id (not a valid ObjectId)
curl http://localhost:5000/tasks/abc
# 404 — well-formed id, but no matching task
curl http://localhost:5000/tasks/665f00000000000000000000
# 404 — undefined route
curl http://localhost:5000/nope
# 415 — missing Content-Type
curl -X POST http://localhost:5000/tasks -d '{"title":"no header"}'
# 500 — deliberately thrown error, to prove the global handler works
curl http://localhost:5000/debug/throwOr import the routes above into Postman/Thunder Client.
- In-memory
let tasks = []array → real persistence in MongoDB via theTaskMongoose model (models/Task.js). - Manual
if (!title) ...checks in the controller → schema-level validation (required,minlength/maxlength) that Mongoose enforces automatically and reports back as aValidationError. validateTaskIdnow checks for a valid MongoObjectIdinstead of a positive integer, since ids are generated by MongoDB, not an in-process counter.errorHandlergained two new branches to translate Mongoose-specific errors (ValidationError,CastError) into the same clean{ error }JSON shape as everything else.- New
config/db.js(connection helper),.env.example(connection string template), andscripts/seed.js(sample data loader).
- Express server on a defined port, confirmed via terminal + curl/Postman
- All 4 CRUD endpoints with correct HTTP methods and status codes (200/201/400/404/415/500)
- Global request-logging middleware (method, URL, timestamp) applied to every request
- Global error-handling middleware, registered last, returns structured JSON and never leaks stack traces to the client
- Mongoose schema with field-level validation (required, length bounds, defaults, timestamps)
- Real MongoDB persistence via
config/db.js, with a fail-fast startup if the DB is unreachable - Content-Type validation on POST/PUT, ObjectId-format validation on
:idroutes, structured 404 handler - Seed script for reproducible sample data