A beginner-friendly CRUD API built for the FlyRank Backend Internship. It started as an in-memory list of tasks (Week 2), moved to a SQLite file (Week 3, A2), and now runs against a containerized PostgreSQL database (Week 1, A3) — with the exact same endpoints and responses at every stage. This is the same repository growing, not three separate projects.
One command starts both the API and its Postgres database:
cp .env.example .env
docker compose upThe API is then available at http://localhost:3000. The tasks table and its three seed rows are created automatically on first boot. To stop everything: docker compose down (add -v if you also want to wipe the volume and start clean).
Environment variables the app needs are documented in .env.example — copy it to .env and adjust if needed. Inside compose.yaml, the API reaches the database by its service name (db), not localhost.
curl -i http://localhost:3000/tasksHTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
[{"id":1,"title":"Learn Express","done":false},{"id":2,"title":"Build a CRUD API","done":false},{"id":3,"title":"Read the assignment","done":true}]
(Screenshot of docker exec -it taskdb psql -U postgres -d tasks -c "SELECT * FROM tasks;", or the same query in a GUI like pgAdmin/DBeaver/TablePlus.)
- Start a real database server:
This runs the official
docker run --name taskdb -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=tasks \ -p 5432:5432 -v taskdata:/var/lib/postgresql/data -d postgres
postgresimage, names the containertaskdb, sets a password and atasksdatabase, maps port 5432 to the host, and mounts a named volume (taskdata) so the data survives even if the container is removed and recreated. - Look inside it:
docker psto confirm it's running, thendocker exec -it taskdb psql -U postgres -d tasksopens apsqlprompt inside the container (\dtlists tables,\qquits). - Secrets: the connection string lives in a git-ignored
.envfile..env.exampleis committed with the same keys and placeholder values so anyone cloning the repo knows what to set. No password is ever hardcoded in the source. - One command for the whole stack:
compose.yamldefines two services —api(built from theDockerfile) anddb(the officialpostgresimage with a named volume).docker compose upstarts both together; the API waits for the database's healthcheck before starting. - Persistence proof: created tasks, ran
docker compose downthendocker compose upagain, and the tasks were still there — the named volume (taskdata) keeps Postgres's data outside the container's own lifecycle.
- Why SQLite: it's a single-file, zero-setup database — no server to install or run, perfect for a small CRUD API like this one, and an easy stepping stone toward Postgres/MySQL later.
- Where the database lives:
tasks.dbin the project root. It's created automatically the first time the server starts (seedb.js), and it's git-ignored so every clone starts fresh. - How to start the project:
npm installthennpm start(same as before) — thetaskstable and the three seed tasks are created automatically if the file or table doesn't exist yet.
Opened tasks.db in DB Browser for SQLite and ran a few queries directly against it — the same file the API reads and writes.
Example query:
SELECT * FROM tasks WHERE done = 1;This returned the seeded "Read the assignment" task. Hitting GET /tasks?done=true through the running API returned the exact same row, with no server restart needed — confirming the API and DB Browser are reading the same live file, not separate copies of the data.
- Docker Desktop (or Podman) — recommended, runs the whole stack
- Node.js 18+ and npm — only needed if running the app outside Docker
With Docker (recommended, one command):
cp .env.example .env
docker compose upWithout Docker (Postgres already running locally on port 5432):
npm install
cp .env.example .env # edit DATABASE_URL if your local Postgres differs
npm startThe API runs at http://localhost:3000 either way.
| Method | Endpoint | Description | Success | Errors |
|---|---|---|---|---|
| GET | / |
API information | 200 | — |
| GET | /health |
Health check (also pings the database) | 200 | 503 |
| GET | /tasks |
List all tasks | 200 | — |
| GET | /tasks/:id |
Get one task | 200 | 404 |
| POST | /tasks |
Create a task | 201 | 400 |
| PUT | /tasks/:id |
Update a task title and/or completion status | 200 | 400, 404 |
| DELETE | /tasks/:id |
Delete a task | 204 | 404 |
| GET | /stats |
Return total, done, and open counts | 200 | — |
| POST | /reset |
Restore the three sample tasks | 200 | — |
| GET | /docs |
Interactive Swagger UI documentation | 200 | — |
GET /tasks?done=truereturns only completed tasks. Usedone=falsefor open tasks.GET /tasks?search=milksearches task titles case-insensitively.GET /tasks?limit=2\&offset=2returns a page of matching tasks.- Filtering, search, and pagination can be combined.
List tasks:
curl -i http://localhost:3000/tasksCreate a task:
curl -i -X POST http://localhost:3000/tasks \\
-H "Content-Type: application/json" \\
-d '{"title":"Buy milk"}'Example response:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"id":4,"title":"Buy milk","done":false}
Filter, search, and paginate:
curl -i "http://localhost:3000/tasks?done=false\&search=express\&limit=2\&offset=0"Get statistics and reset the in-memory data:
curl -i http://localhost:3000/stats
curl -i -X POST http://localhost:3000/resetUpdate a task:
curl -i -X PUT http://localhost:3000/tasks/4 \\
-H "Content-Type: application/json" \\
-d '{"done":true}'Delete a task:
curl -i -X DELETE http://localhost:3000/tasks/4Open http://localhost:3000/docs while the server is running. Use Try it out to complete the full CRUD cycle and try the optional endpoints without curl.
The OpenAPI document is generated from the route comments with swagger-jsdoc when the server starts and is served through swagger-ui-express. The generated document is also saved as openapi.json.
Try it yourself: run Postgres without a volume (docker run -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=tasks -p 5432:5432 -d postgres, no -v flag), create a few tasks, then docker rm -f that container and start a fresh one — the tasks will be gone. A container's own filesystem dies with the container; a named volume is what keeps the database's files on disk independently of any one container's lifecycle, which is why compose.yaml mounts taskdata:/var/lib/postgresql/data for the db service instead.
Build a beginner-friendly JavaScript Node.js API using Express.js only. Use port 3001 and an in-memory array with three sample tasks containing numeric id, string title, and boolean done fields. Implement GET /, GET /health, GET /tasks, GET /tasks/:id, POST /tasks, PUT /tasks/:id, and DELETE /tasks/:id. POST must require a non-empty title, assign the next id, set done to false, and return 201. PUT must allow title and/or done updates, reject an empty or invalid body with 400, and return 404 for an unknown id. DELETE must return 204 with no body and 404 for an unknown id. Return JSON errors and keep the code beginner-friendly with no database, authentication, TypeScript, ORM, or unnecessary folders. Also include Swagger UI at /docs using swagger-ui-express and document the API with an OpenAPI file.
- The AI version combines the POST title validation messages, while the hand-built version distinguishes “Title is required” from “Title cannot be empty.”
- The AI version uses a separate port, 3001, so it can be run beside the hand-built API for comparison.
- The hand-built version includes the optional filtering, search, statistics, reset, pagination, and Swagger-jsdoc features; the quarantined AI version contains only the core Stage 0–6 CRUD API.
The AI version was run on port 3001 and tested with the Stage 4 CRUD requests. The hand-built version remained untouched while the AI version was reviewed.
The second prompt added the exact validation-message distinction and the requirement to keep the optional query and statistics endpoints. The hand-built version already satisfies those additional requirements, so no change was made to the hand-built code after the review.
task-api/
├── package.json
├── package-lock.json
├── index.js
├── openapi.json
├── README.md
├── ai-version/
│ └── index.js
└── screenshots/
└── swagger.png
- New tasks require a non-empty
title; invalid input returns400. - Updates must include
titleand/ordone; empty or invalid input returns400. - Unknown task ids return
404with a JSON error. - Successful reads and updates return
200. - Successful creation returns
201. - Successful deletion returns
204with no response body.

