A reference implementation of a three-tier web application: a Next.js frontend, an Express REST API, and a PostgreSQL database. It runs locally with Docker Compose and deploys to Google Cloud Platform (Cloud Run + Cloud SQL) via Terraform.
Browser → Web (Next.js :3000) → API (Express :3001) → PostgreSQL
| Layer | Technology | Location |
|---|---|---|
| Frontend | Next.js 16, React 19, Tailwind CSS | src/web/ |
| API | Express 5, Node.js 22 | src/api/ |
| Database | PostgreSQL 17 | managed by Docker / Cloud SQL |
| Migrations | node-pg-migrate | src/db/ |
| Infrastructure | Terraform (GCP) | src/infrastructure/ |
The app is a simple task manager (to-do list) that demonstrates how the three tiers communicate.
- Docker Desktop (or Docker Engine + Compose plugin)
docker compose up --buildThis starts four services in order:
- postgres — PostgreSQL 17 database, waits until healthy
- migrate — runs
node-pg-migrate upto apply schema migrations, then exits - api — Express API on port 3001 (internal only)
- web — Next.js frontend on port 3000 (exposed to host)
Once running, open http://localhost:3000.
# Stop containers (keeps the postgres_data volume)
docker compose down
# Stop and delete all data
docker compose down -vdocker compose up --buildThe API is not exposed directly, but you can reach it through the web container or by temporarily mapping its port:
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /tasks |
List all tasks |
| POST | /tasks |
Create a task ({ "title": "..." }) |
| PATCH | /tasks/:id |
Update a task ({ "completed": true } or { "title": "..." }) |
src/
├── api/ # Express REST API
│ ├── index.js # Route handlers
│ ├── db.js # PostgreSQL connection pool
│ └── Dockerfile
├── db/ # Database migrations
│ ├── migrations/ # node-pg-migrate migration files
│ └── Dockerfile
├── web/ # Next.js frontend
│ ├── app/ # App Router pages and components
│ └── Dockerfile
└── infrastructure/ # Terraform for GCP deployment
├── main.tf
├── variables.tf
└── outputs.tf
The src/infrastructure/ directory contains Terraform that provisions:
- VPC network and subnet
- Cloud SQL PostgreSQL 17 instance (private IP)
- Cloud Run services for the API and web frontend
- Secret Manager secret for the database URL
- Service accounts and IAM bindings
| Variable | Description |
|---|---|
project_id |
GCP project ID |
api_image |
Container image URI for the API (e.g. gcr.io/PROJECT/api:TAG) |
web_image |
Container image URI for the web frontend |
region |
GCP region (default: us-central1) |
environment |
dev, staging, or prod (default: dev) |
cd src/infrastructure
terraform init
terraform apply -var="project_id=my-project" \
-var="api_image=gcr.io/my-project/api:latest" \
-var="web_image=gcr.io/my-project/web:latest"After apply, terraform output web_url gives the public URL.
Migrations live in src/db/migrations/ and use node-pg-migrate.
# Apply all pending migrations (run inside the db container or with DATABASE_URL set)
cd src/db
DATABASE_URL=postgres://app:app@localhost:5432/app npx node-pg-migrate up
# Roll back the last migration
DATABASE_URL=postgres://app:app@localhost:5432/app npx node-pg-migrate downWhen running via Docker Compose the migrate service handles this automatically on startup.