Fullstack starter: NestJS API + Next.js web app, in one repo, with automated GitHub Actions CI/CD pipeline deploying to AWS EC2.
jsstack/
├── backend/ NestJS API (Postgres + Redis, Prisma, Swagger)
├── frontend/ Next.js 16 App Router web app
├── docker-compose.yml Full local stack for end-to-end testing
└── .github/workflows/ci.yml CI: test → build → push images
- 🚀 Fullstack TypeScript - NestJS backend + Next.js frontend
- 📦 Monorepo - Single repo with shared tooling
- 🧪 Testing - Jest with Codecov coverage reporting
- 🐳 Docker - Containerized development and production
- 🔄 CI/CD - GitHub Actions automated pipeline to AWS EC2
- 📊 Monitoring - Health checks, logging, and error tracking
- 🔒 Security - Rate limiting, CORS, environment validation
This is the fastest way to prove the whole stack works end-to-end before touching the VPS.
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
docker compose up --build| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| Backend API | http://localhost:4000/api |
| Swagger docs | http://localhost:4000/docs |
| Health (live) | http://localhost:4000/health |
| Health (ready) | http://localhost:4000/health/ready |
| Postgres | localhost:5432 (jsstack/jsstack) |
| Redis | localhost:6379 |
The migrate service runs once on startup and applies Prisma migrations
against Postgres before api is considered ready. Visit
http://localhost:3000/users to exercise a full create+list round trip
through the browser, and the home page (/) to confirm server-side
(Server Component) connectivity to the backend.
To reset the local database entirely:
docker compose down -v
docker compose up --buildpnpm install # installs both backend/ and frontend/ workspace packages
# Terminal 1 — needs Postgres + Redis reachable at localhost (e.g. via
# `docker compose up postgres redis`)
pnpm dev:backend
# Terminal 2
pnpm dev:frontendTwo different base URLs are used depending on where the code runs — see
frontend/src/lib/api.ts:
- Browser-side (
'use client'components, like/users): usesNEXT_PUBLIC_API_URL, baked into the JS bundle at build time. This must be a URL the browser can actually reach — the public domain in production,localhost:4000locally. - Server-side (Server Components, like the home page health check):
uses
API_BASE_URL_SERVER, read at runtime. In Docker/production this points straight at theapicontainer over the internal network (http://jsstack-staging-api:4000/api), skipping Caddy entirely.
.github/workflows/ci.yml:
- Test & build — backend and frontend run independently (type-check,
lint, test with coverage, build). PRs and pushes to
main/developtrigger this; nothing is deployed yet. - Build & push images — only on
workflow_dispatchor a push todevelop. Buildsbackend/Dockerfile→ghcr.io/.../jsstack/apiandfrontend/Dockerfile→ghcr.io/.../jsstack/web, tagged by environment and short SHA.
main only deploys when you manually trigger the workflow and choose
production or staging — pushing to main alone does not deploy.
Pushing to develop auto-deploys to staging.
- Backend: health checks (
/health,/health/ready), ausersmodule (entity + DTO + service + controller) wired to Postgres via Prisma, Redis caching via@nestjs/cache-manager, rate limiting, Swagger at/docs, a runnable migration + seed script. - Frontend: App Router, a server-rendered home page proving backend
connectivity, a client-rendered
/userspage exercising a full create+list round trip, standalone Docker output for a minimal runtime image.
Extend backend/src/modules/ and frontend/src/app/ for real features —
the wiring (env validation, Docker, CI) is already done.
- Swagger docs
- User Module with auth and Dashboard
- CD via Terraform
- K8s
- Rate Limiting
- Caching