Skip to content

Repository files navigation

📋 Task API

A CRUD API for managing a to-do list, built with Node.js and Express as part of the FlyRank Backend Internship (Weeks 2–4, Assignments A1–A3 + layered architecture refactor).

Full Create, Read, Update, Delete on tasks, interactive Swagger docs, and two interchangeable storage backends — SQLite and containerized Postgres — running through a single, unified entry point. One DATABASE_TYPE setting decides which one runs; the routes, validation, and API behavior never change.


🧱 Tech stack

  • Node.js + Express 5 — server and routing (Express 5 auto-forwards async errors, no manual try/catch needed in routes)
  • better-sqlite3 — synchronous SQLite driver
  • pg — PostgreSQL driver
  • redis — Redis client, used for a real connectivity + health check (Postgres mode only)
  • swagger-ui-express — interactive docs from a hand-written OpenAPI 3.0 spec (openai.json)
  • cors — allows browser-based tools (e.g. Hoppscotch) to call the API from a different origin
  • Docker + Docker Compose — containerized Postgres + Redis stack

🗂️ Project structure

├── index.js # single entry point — reads DATABASE_TYPE, wires the matching repo, starts the app
├── src/
│ ├── app.js # Express app factory — mounts routes + middleware, backend-agnostic
│ ├── errors.js # typed error classes (ValidationError, NotFoundError, ConflictError)
│ ├── middleware/
│ │ └── error-handler.js # central error → status code mapping
│ ├── routes/
│ │ ├── task.routes.js # /tasks routes — thin, delegate to services
│ │ └── meta.routes.js # /, /health
│ ├── services/
│ │ └── tasks.service.js # business logic — validation, duplicate checks, stats — shared by both backends
│ ├── repositories/
│ │ ├── taskRepository.js # SQLite data access
│ │ └── taskRepository.postgres.js # Postgres data access — same function signatures
│ └── db/
│ ├── db.sqlite.js # SQLite connection, table creation, seeding
│ ├── db.postgres.js # Postgres connection, table creation, seeding
│ └── db.redis.js # Redis client, connection, ping
├── Dockerfile # builds the Postgres app image
├── compose.yaml # api + db + redis services, one-command startup
├── tasks.db # SQLite file (gitignored)
├── .env / .env.example # DATABASE_TYPE, PORT, Postgres credentials
├── openai.json # OpenAPI spec powering /docs
├── package.json
└── README.md


🚀 Quick start

git clone https://github.com/davidyassa/CRUD_API.git
cd CRUD_API
cp .env.example .env

SQLite (no Docker needed)

Requires: Node.js 18+

# in .env: DATABASE_TYPE=sqlite
npm install
npm start

tasks.db is created and seeded automatically on first run — no database setup needed.

Postgres (Docker)

Requires: Docker Desktop running. No local Node.js install needed — the app runs entirely inside the container.

# in .env: DATABASE_TYPE=postgres
docker compose up

Compose builds the app image, starts Postgres + Redis, creates the tasks table, seeds it, and starts the API — all in one command.

Either way, the API runs on http://localhost:3000 (set by PORT in .env) · Swagger docs at http://localhost:3000/docs.

Switching backends: change DATABASE_TYPE in .env, then restart with npm start (SQLite) or docker compose up (Postgres). Only one runs at a time — both use the same port.

Confirm Postgres persistence: create a task, then docker compose down followed by docker compose up again — the task is still there, because the named volume (taskdata) kept the data.

(optional) Browsing the Postgres database directly: connect a free GUI (TablePlus, DBeaver, pgAdmin) with:

Field Value
Host localhost
Port 5432
User postgres
Password matches POSTGRES_PASSWORD in your .env
Database matches POSTGRES_DB in your .env

🧪 Testing the API (no coding required)

Option 1 — Swagger UI (built in, zero setup)

Open http://localhost:3000/docs. Click any endpoint to expand it, hit "Try it out", fill in the fields, click "Execute".

Option 2 — Hoppscotch (a free API-testing tool)

  1. Go to hoppscotch.io — no install, no account needed.

    If requests fail with a network/CORS error, install the Hoppscotch browser extension — this app already sends CORS headers, but some browser setups still need it for localhost.

  2. Set the method dropdown to match the endpoint, paste the URL, e.g. http://localhost:3000/tasks.
  3. For POST/PUT: click BodyJSON:
   { "title": "Buy milk" }
  1. Click Send.

Quick things to try:

Try this Method + URL Body
List all tasks GEThttp://localhost:3000/tasks
Get one task GEThttp://localhost:3000/tasks/1
Create a task POSThttp://localhost:3000/tasks { "title": "Walk the dog" }
Mark it done PUThttp://localhost:3000/tasks/4 { "done": true }
Delete it DELETEhttp://localhost:3000/tasks/4

📡 Endpoints

Method Path Description
GET / API description — name, active database type, available endpoints
GET /health Reports API status; on Postgres also checks db (SELECT 1) and redis (PING) — 200/503
GET /tasks List all tasks — supports ?done=true|false and ?search=term
GET /tasks/:id Get a single task by id
GET /stats Task counts: total, completed, remaining
POST /tasks Create a task ({ "title": "..." })
POST /reset SQLite only — clears and reseeds the table
PUT /tasks/:id Update a task's title and/or done; if nothing actually changed, returns the task with an added message: "no change" field, still 200
DELETE /tasks/:id Delete a task

Status codes

Code Meaning
200 Successful read/update
201 Task created
204 Task deleted (empty body)
400 Invalid or missing input
404 Task with that id doesn't exist
409 A task with that title already exists (create/update)
503 /health reports a degraded dependency (Postgres mode)

🏗️ Architecture

A layered structure keeps storage swappable without touching routes or business rules:

routesservicesrepositories → database

  • Routes (src/routes/) are thin — they parse the request and call a service, nothing more.
  • Services (src/services/tasks.service.js) own all business logic — validation, duplicate-title checks, not-found handling, stats aggregation. Written once, shared by both backends via TaskServices(repo), a factory that takes whichever repository index.js injects.
  • Repositories (src/repositories/) are pure data access — no validation, no rules — just run a query and return the result. taskRepository.js and taskRepository.postgres.js expose identical function signatures, so this is the only layer that changes between backends.
  • Errors (src/errors.js + src/middleware/error-handler.js) — typed error classes (ValidationError, NotFoundError, ConflictError) thrown from the service, caught in one central place and mapped to status codes. Express 5 auto-forwards thrown/rejected errors from async route handlers, so no per-route try/catch is needed.

index.js is the only file that knows both backends exist: it reads DATABASE_TYPE, requires the matching repository and db connection, and hands everything else the same app factory.

Postgres version note: pinned to postgres:16, not the default postgres (18+) tag — the 18+ image changed its data-directory layout in a way that's incompatible with a simple single-mount volume, which caused a startup error on the newer default.

Lean container image: the Dockerfile only installs the packages the app actually needs (express, pg, swagger-ui-express, dotenv, cors, redis) instead of the full package.json — this deliberately skips better-sqlite3, which needs native compilation tools not present in the lightweight node:22-alpine base image, and isn't needed in the Postgres container anyway.

Secrets: DATABASE_URL is never hardcoded — in Compose it's assembled from POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, and PORT env vars (.env, gitignored; placeholder values committed to .env.example).


🖥️ Swagger UI

Swagger UI screenshot

🔍 Database viewers

SQLite, via DB Browser for SQLite:

Database viewer screenshot

Postgres, via TablePlus:

TablePlus screenshot

🩺 Health check in action

Healthy state (Postgres mode):

{ "status": "ok", "db": "ok", "redis": "ok" }

Degraded state — Redis stopped (docker compose stop redis), API stays up but reports it:

Health check with Redis down

🧪 Example SQL query

Run manually against the SQLite database in DB Browser:

DELETE FROM tasks WHERE done = 1;

Clears completed tasks — a direct, visible confirmation that manual database changes take effect immediately and are reflected the next time the API reads from it, with no code change required.


✨ Extras built beyond the core spec

  • Query filtering — GET /tasks?done=true and GET /tasks?search=milk (combinable)
  • Duplicate-title check (409) on both POST /tasks and PUT /tasks/:id
  • No-op update detection on PUT /tasks/:id — flags when a request changes nothing
  • Layered architecture (routes/services/repositories/errors) — one service and one set of routes shared by both backends
  • Real /health check on Postgres — pings the database (SELECT 1) and Redis (PING) rather than returning a static "ok"; returns 503 if either dependency is down
  • Redis added to the Docker Compose stack, connected on startup, included in the health check
  • Single unified entry point (index.js) and shared PORT — switch backends via one .env value, no separate scripts or ports to remember

⚠️ Worth noting

  • No authentication (yet) — local development APIs, not production-hardened.
  • POST /reset is SQLite-only by design — the Postgres version exists specifically to demonstrate persistence, and a reset endpoint cuts against that story.
  • SQLite is single-writer — fine at this scale, exactly why the Postgres version exists.
  • Only one backend runs at a time; switching requires changing DATABASE_TYPE and restarting.

About

CRUD API for managing tasks — Node.js + Express, full REST endpoints, in-memory storage, Swagger docs.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages