Skip to content
WilburStanleyPublic

About

TypeScript CRUD API on Express, swappable between Postgres (Docker) and SQLite. Zod-validated input, parameterized queries, centralized error handling, Swagger docs at /docs.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

CRUD TASK API

A simple CRUD API for managing to-do tasks, built with TypeScript, Express, and Node.js. Data persists in Postgres, running in Docker with a volume. SQLite remains available as a no-Docker-required fallback for local dev.

Storage: Postgres (Docker) or SQLite

The app supports two interchangeable storage backends, chosen at startup by the DB_DRIVER env var:

  • postgres (default when using Docker) — a real Postgres database, running in a container with a persistent volume. This is the primary storage for this project.
  • sqlite — a local file (tasks.db), zero setup, useful for quick local dev without Docker running.

Both implement the exact same TaskRepository interface (src/repositories/task.repository.ts), so the service layer (src/services/tasks.service.ts) and controller/routes never change based on which one is active — swapping storage is a one-file decision, made in src/repositories/index.ts.

Honest note on the architecture

This project originally queried SQLite directly inside the controller — there was no repository interface or service layer to "swap," despite the app already persisting data to disk. As part of adding Postgres, I first refactored that: extracted the TaskRepository interface, moved the existing SQLite logic behind it (sqlite-task.repository.ts), and added a thin service layer so the controller only calls service functions and never touches SQL directly.

Only after that existed did I add postgres-task.repository.ts and wire it in via src/repositories/index.ts. That factory file is the entire "swap" — tasks.controller.ts, tasks.routes.ts, and tasks.service.ts are identical regardless of which backend is running.

Running with Docker + Postgres

cp .env.example .env
docker compose up --build

This starts two containers:

  • db — Postgres 16, with a named volume (pgdata) so data survives container restarts, and docker/init.sql creating the tasks table on first boot.
  • app — this API, built from the Dockerfile, waiting for db's healthcheck before it starts.

The API is reachable at http://localhost:6767, same as running it without Docker.

To stop everything but keep the data: docker compose down To stop everything and delete the data (fresh start): docker compose down -v

Running with SQLite instead (no Docker)

Set DB_DRIVER=sqlite in .env (or leave DB_DRIVER unset — it's the default), then:

npm install
npm run dev

Server runs at http://localhost:6767. The database file (tasks.db) and its table are created automatically on first run, seeded with 3 example tasks. tasks.db is git-ignored, so every clone starts fresh with the seeded data.

Proving persistence

  1. Started the stack: docker compose up --build. Confirmed the app logged [db] using postgres repository.
  2. Created a task via POST /tasks, {"title":"survive a restart"} — came back as id: 4.
  3. Confirmed it via GET /tasks.
  4. Fully tore down the stack: docker compose down (no -v, so the pgdata volume was kept while both containers were deleted).
  5. Brought it back up: docker compose up --build. Fresh containers, same volume.
  6. GET /tasks again — task id: 4 was still there, unchanged, because it lives in the pgdata volume rather than in the containers themselves.

Env vars

Var Used by Purpose
DB_DRIVER app sqlite or postgres — selects the repository
SQLITE_FILE app (sqlite mode) path to the local SQLite file
DATABASE_URL app (postgres mode) Postgres connection string. Inside docker compose, the host is db (the service name), not localhost
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB Postgres container read once, on first boot, to create the user/database

Endpoints

Method Path Description
GET / API info
GET /health Health check
GET /tasks List all tasks
GET /tasks/:id Get a single task
POST /tasks Create a task
PUT /tasks/:id Update a task's title and/or done
DELETE /tasks/:id Delete a task
POST /tasks/reset Reset to the 3 seed tasks

API docs

Interactive Swagger UI: http://localhost:6767/docs

Example

curl -i -X POST http://localhost:6767/tasks -H "Content-Type: application/json" -d '{"title":"Buy milk"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{"id":4,"title":"Buy milk","done":false}

Exploring the database directly

Opened tasks.db via the sqlite3 CLI and ran:

SELECT * FROM tasks WHERE done = 1;

Returned one row: 3|Finish assignment|1 - the only task marked done at the time.

DB Browser screenshot

Swagger UI

Swagger UI screenshot

About

TypeScript CRUD API on Express, swappable between Postgres (Docker) and SQLite. Zod-validated input, parameterized queries, centralized error handling, Swagger docs at /docs.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages