A production-minded full-stack voting app built for learning. Single question polls, JWT auth via httpOnly cookies, Redis caching, and clean layered architecture.
- Runtime: Node.js + Express + TypeScript
- Database: PostgreSQL (via Prisma ORM)
- Cache: Redis (ioredis)
- Auth: JWT in httpOnly cookies
- Validation: Zod
- Logging: Pino + Pino Pretty (dev)
- Node.js 18+
- Docker + Docker Compose
1. Clone and install
cd backend
npm install2. Set up environment
cp .env.example .envEdit .env with your values:
PORT=3000
DATABASE_URL="postgresql://quickpoll:quickpoll@localhost:5432/quickpoll"
REDIS_URL="redis://localhost:6379"
JWT_SECRET="change_this_in_production"
NODE_ENV=development
FRONTEND_URL=http://localhost:51733. Start Docker services
# from project root
docker compose up -dThis starts:
- PostgreSQL on port
5432 - Redis on port
6379 - pgAdmin on port
8080(admin@quickpoll.com / admin)
4. Run migrations
npx prisma migrate dev5. Start the dev server
npm run devServer runs on http://localhost:3000.
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /auth/register |
❌ | Register new user |
| POST | /auth/login |
❌ | Login, sets httpOnly cookie |
| POST | /auth/logout |
❌ | Clears auth cookie |
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /polls |
✅ | Create a poll |
| GET | /polls/:id |
❌ | Get poll by ID |
| GET | /polls/:id/results |
❌ | Get poll results (Redis cached) |
| POST | /polls/:id/vote |
✅ | Cast or change a vote |
| Method | Route | Description |
|---|---|---|
| GET | /health |
Server health check |
src/
routes/ # Express routers
controllers/ # Request/response handlers
services/ # Business logic
middleware/ # Auth, logging, error handling
validators/ # Zod schemas
utils/ # Logger, Redis client, env, AppError
prisma/
schema.prisma # DB schema
client.ts # Prisma singleton
migrations/ # Migration history
Request → requestLogger middleware
→ Route
→ authenticate middleware (protected routes)
→ Controller (parse + validate)
→ Service (business logic + DB/Redis)
→ Response
Error anywhere → errorHandler middleware
→ logs to console (pino)
→ attempts DB log (errors only)
→ returns { error, requestId }
- httpOnly cookies for JWT — JS can't read it, safer than localStorage
- Redis cache-aside pattern — results cached for 5 min, invalidated on vote
- Zod env validation — app refuses to start if env vars are missing or malformed
- Single PrismaClient instance — prevents connection pool exhaustion
- Custom AppError class — distinguishes known errors from unexpected ones
- requestId on every request — UUIDs for tracing errors across logs
npm run dev # Start with nodemon (hot reload)
npm run build # Compile TypeScript
npm run start # Run compiled JS
npx prisma studio # Visual DB browserBuild the frontend first:
cd ../frontend && npm run buildThen set NODE_ENV=production in .env and run:
npm run build && npm run startThe backend serves the frontend static files from frontend/dist/ in production.