A modern, accessible job application portal built with Node.js, TypeScript, Express, Nunjucks, Tailwind CSS, and DaisyUI.
- Authentication & Authorization - Login, registration, session management, admin role controls
- Job Management - Browse, create, edit, delete job roles with status badges
- Applications - Submit with file uploads, track applicants, CSV export
- Responsive UI - Mobile-optimized design with accessibility features
- Modern Stack - TypeScript strict mode, Express, Nunjucks, Tailwind CSS, Axios
- Quality - 242 passing tests, Biome formatting/linting, 80%+ coverage
src/
βββ controllers/ # HTTP handlers (job roles, auth, applications)
βββ services/ # Business logic & API calls (Axios)
βββ middleware/ # Auth middleware & role checking
βββ models/ # TypeScript interfaces & types
βββ utils/ # Validators, CSV export, URL builders
βββ views/ # Nunjucks templates
βββ styles/ # Tailwind CSS input
public/css/ # Compiled CSS output
dist/ # Compiled TypeScript
| Command | Purpose |
|---|---|
npm run dev |
Dev server with hot reload |
npm run build |
Production build |
npm run start |
Start production server |
| Command | Purpose |
|---|---|
npm run test |
Unit tests in watch mode (Vitest) |
npm run test:run |
Run all unit tests once |
npm run test:coverage |
Generate coverage report |
npm run e2e |
Run E2E tests (Playwright) |
npm run e2e:run |
Run E2E tests with HTML report |
npm run e2e:ui |
Run E2E tests in UI mode |
npm run e2e:debug |
Run E2E tests in debug mode |
npm run e2e:report |
View last E2E test report |
| Command | Purpose |
|---|---|
npm run type-check |
TypeScript validation |
npm run check |
Format + lint (run before commits) |
npm run lint |
Lint with Biome |
npm run format |
Format with Biome |
npm install
npm run dev # Start local server
npm run test # Run tests
npm run check # Pre-commit checksFor full-stack setup (frontend + backend):
# From project root
docker-compose up -d
# View logs
docker-compose logs -f
# Stop containers
docker-compose downThis will:
- β Start backend on http://localhost:8000
- β Start frontend on http://localhost:3000
- β Automatically configure backend API URLs
- β Create a shared Docker network for communication
# Build the image
docker build -t team2-job-app-frontend:latest .
# Run with backend on Docker network
docker network create app-network
docker run -p 3000:3000 \
--network app-network \
-e NODE_ENV=production \
-e SESSION_SECRET=your-secret-key \
-e API_BASE_URL=http://team2-backend:8000 \
-e AUTH_API_BASE_URL=http://team2-backend:8000/api/auth \
team2-job-app-frontend:latest
# Run with backend on host machine
docker run -p 3000:3000 \
-e NODE_ENV=production \
-e SESSION_SECRET=your-secret-key \
-e API_BASE_URL=http://host.docker.internal:8000 \
-e AUTH_API_BASE_URL=http://host.docker.internal:8000/api/auth \
team2-job-app-frontend:latest- β Multi-stage build - Optimized for production (~231MB)
- β Minimal base image - Node.js 18 Alpine
- β Security - Runs as non-root user (appuser:1001)
- β Health checks - Built-in health monitoring
- β Production ready - Only production dependencies included
| Variable | Description | Docker Compose | Manual Run |
|---|---|---|---|
NODE_ENV |
Environment mode | production |
production |
SESSION_SECRET |
Secret key for sessions | Set in compose | your-secret-key |
API_BASE_URL |
Backend API URL | http://team2-backend:8000 |
See examples above |
AUTH_API_BASE_URL |
Auth API URL | http://team2-backend:8000/api/auth |
See examples above |
Inside Docker Network (backend also in Docker):
API_BASE_URL=http://team2-backend:8000
AUTH_API_BASE_URL=http://team2-backend:8000/api/authHost Machine (Docker Desktop on Mac/Windows):
API_BASE_URL=http://host.docker.internal:8000
AUTH_API_BASE_URL=http://host.docker.internal:8000/api/auth# View logs
docker logs team2-frontend
# Check health status
docker inspect --format='{{json .State.Health}}' team2-frontend
# Access container shell
docker exec -it team2-frontend sh
# Test backend connectivity from container
docker exec -it team2-frontend curl http://team2-backend:8000/health
# Rebuild without cache
docker build --no-cache -t team2-job-app-frontend:latest .The project uses GitHub Actions for continuous integration and deployment on all branches:
1. Code Quality Checks (code-quality)
Runs on every push and pull request:
- β TypeScript type checking
- β Biome format validation
- β Biome linting checks
- β Unit tests with coverage
- β Production build verification
- β Uploads coverage reports (7-day retention)
- β Uploads build artifacts (7-day retention)
2. Docker Build (docker-build)
Runs after quality checks pass on all branches:
- π³ Builds Docker container image
- π·οΈ Multi-tag strategy (SHA, branch, latest)
- πΎ Layer caching for faster builds
- β Container startup validation
- π Build information display
3. Push to Azure Container Registry (push-to-acr)
Runs after Docker build succeeds, only on main branch pushes:
- π Authenticates with Service Principal credentials
- π€ Pushes image to Azure Container Registry (ACR)
- π·οΈ Tags images with git SHA and
main-latestfor main branch - β Provides pull commands for deployment
- βοΈ Skipped for PRs and non-main branches (cost optimization)
4. Terraform Plan & Apply (terraform)
Runs after ACR push, plan on all branches, apply only on main:
- ποΈ Initializes Terraform with remote state
- π Plans infrastructure changes
- β Applies changes to Azure (main branch only)
- π Uses Service Principal for Azure authentication
- πΎ State managed in Azure Storage (team collaboration ready)
Local Build Tags (all branches):
team2-job-app-frontend:abc1234 # Git SHA (always created)
team2-job-app-frontend:main # Branch name (always created)
team2-job-app-frontend:latest # Latest stable (main branch only)ACR Registry Tags (main branch only):
myacr.azurecr.io/team2-job-app-frontend:abc1234 # Specific commit
myacr.azurecr.io/team2-job-app-frontend:main-latest # Latest from mainTag Purposes:
- Git SHA (
abc1234): Unique identifier for each commit, enables rollback - Branch name (
main,feature-login): Easy reference for branch-specific builds main-latest: Always points to the latest stable version on main branch- ACR registry: Only pushed to for main branch merges (cost optimization)
| Metric | Cold Build | Cached Build |
|---|---|---|
| Duration | ~2-3 minutes | ~30-60 seconds |
| Cache Strategy | GitHub Actions cache | Layer reuse |
| Timeout | 10 minutes | 10 minutes |
| ACR Push Time | ~30-60 seconds | β |
Optimization Features:
- GitHub Actions cache for Docker layers (
cache-from: type=gha) - Multi-stage Dockerfile reduces final image size
- Parallel job execution when possible
- ACR push only on main branch (avoids unnecessary registry bloat)
If the Docker build fails:
- β The workflow stops and marks the check as failed
- π Build logs are available in the GitHub Actions UI
- π§ͺ Container startup test provides immediate feedback
- β»οΈ Previous successful images remain available
- π GitHub sends notification to commit author
Common Failure Scenarios:
- Dockerfile syntax errors
- Missing dependencies in build stage
- Container startup failures
- Health check timeouts
If ACR push fails (main branch only):
- β Docker image built successfully but ACR push failed
- π Check ACR credentials in GitHub secrets
- π Verify Azure Container Registry is accessible
- π Review ACR authentication logs
To enable pushing to Azure Container Registry, configure these GitHub secrets in your repository settings (Settings > Secrets and variables > Actions):
| Secret | Value | Description |
|---|---|---|
ACR_REGISTRY |
myacr.azurecr.io |
Your Azure Container Registry URL (e.g., myregistry.azurecr.io) |
ACR_USERNAME |
Service Principal ID | Service Principal appId for authentication |
ACR_PASSWORD |
Service Principal Password | Service Principal password/secret |
Setting Up Service Principal:
Use the Azure CLI to create a service principal with push permissions:
# Create service principal with ACR push role
az ad sp create-for-rbac --name "team2-job-app-sp" \
--role acrpush \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ContainerRegistry/registries/{registry-name}
# Output will contain:
# "appId": "YOUR_CLIENT_ID" <- Use as ACR_USERNAME
# "password": "YOUR_CLIENT_SECRET" <- Use as ACR_PASSWORD
# "tenant": "YOUR_TENANT_ID"Security Best Practices:
- β Use Service Principal (not admin credentials) - least privilege
- β Rotate credentials periodically
- β
Scope permissions to only ACR push (
acrpushrole) - β Store secrets in GitHub encrypted secrets (never in code)
- β Use separate service principal per project for isolation
Before pushing, run the same checks locally to catch issues early:
# Code quality checks
npm run type-check # TypeScript validation
npm run check # Biome format + lint
npm run test:run # All unit tests
npm run build # Production build
# Docker build (replicates CI)
docker build -t team2-job-app-frontend:local .
docker run -p 3000:3000 team2-job-app-frontend:local
# Test image startup
docker ps | grep team2-job-app-frontend.github/workflows/code-quality.yml
After merging to main, verify the image was pushed to ACR:
# List images in ACR
az acr repository list --name myacr
# List tags for an image
az acr repository show-tags --name myacr --repository team2-job-app-frontend
# Pull image from ACR
docker pull myacr.azurecr.io/team2-job-app-frontend:main-latest
# Run container from ACR
docker run -p 3000:3000 myacr.azurecr.io/team2-job-app-frontend:main-latestTerraform configuration in infrastructure/ folder with dev/prod environments.
cd infrastructure
terraform plan -var-file="terraform.dev.tfvars"
terraform apply -var-file="terraform.dev.tfvars"Workflow: Plan on PRs β Apply on main branch push (via GitHub Actions)
Runtime & Language: Node.js 18+, TypeScript 5.9+ (strict mode) Framework: Express 5.1+, Nunjucks templates Frontend: Tailwind CSS 4, DaisyUI 5.1, Lucide icons API: Axios 1.12, Express Session Testing:
- Unit/Integration: Vitest (242 tests, 80%+ coverage)
- E2E: Playwright (cross-browser testing) Quality: Biome (formatter/linter), ES Modules
- Email/password login and registration
- Session-based role management (Admin/Applicant)
- Personalized success messages
- Password strength validation
- Secure HTTP-only cookies
- Browse job listings with status badges
- View role details with requirements
- Apply for open positions
- Responsive card layout with animations
- Create, edit, delete job roles
- Manage role status (Open/Closed)
- CSV export for reports
- Form validation with clear errors
- Submit applications with file uploads (PDF, DOC, DOCX)
- View applicant list with pagination
- Download resumes and read cover letters
- Status tracking with color-coded badges
npm run test # Watch mode
npm run test:run # Single run
npm run test:coverage # Coverage reportPlaywright provides cross-browser E2E testing with:
- Multi-browser testing: Chromium, Firefox, WebKit
- Mobile testing: Pixel 5, iPhone 12 emulation
- Screenshots & videos: Captured on test failures
- Trace recording: Full trace for debugging
npm run e2e # Run in headless mode
npm run e2e:ui # Interactive UI mode
npm run e2e:debug # Debug mode with inspector
npm run e2e:report # View HTML test reportE2E Test Location: tests/e2e/**/*.spec.ts
npm run type-checkβ No TypeScript errorsnpm run checkβ Biome formatting & linting passesnpm run test:runβ All tests pass
- MVC architecture (Controllers β Services β Models)
- Dependency injection for testability
- Named exports (ES modules)
- No
anytypes (TypeScript strict mode) - Try/catch error handling in controllers
- 80%+ coverage target for new code
.github/instructions/- Project standards & guidelinesdocs/axios-usage-example.md- API integration examplesspec/- Feature specification documents
Backend API runs on http://localhost:8000/api
Frontend dev server runs on http://localhost:3000
Add .env if needed for custom API endpoints:
API_BASE_URL=http://localhost:8000
Kainos 2025 !!