A robust, production-ready RESTful API for tracking job applications, built with Fastify, TypeScript, PostgreSQL, and Redis. This application provides a complete backend solution for managing job application workflows with authentication, rate limiting, activity logging, and real-time statistics.
- Features
- Tech Stack
- Architecture Overview
- Project Structure
- Database Schema
- API Endpoints
- Authentication & Security
- Application Status Workflow
- Getting Started
- Environment Variables
- Available Scripts
- Testing
- API Documentation
- Contributing
- π JWT-based Authentication - Secure access/refresh token flow with cookie storage
- π Application Tracking - Full CRUD operations for job applications
- π Status State Machine - Enforced valid transitions between application statuses
- π Activity Logging - Automatic audit trail for all status changes
- π Statistics Dashboard - Real-time analytics on application data
- π‘οΈ Rate Limiting - Configurable per-route and global rate limits
- π« Comprehensive Error Handling - Standardized error responses with proper HTTP codes
- π Security Headers - Helmet.js for enhanced security
- π§ͺ Test Coverage - Extensive unit and integration tests with Vitest
- π OpenAPI Specification - Fully documented API with OpenAPI 3.0
- π― Type Safety - End-to-end TypeScript with Zod validation
| Category | Technology |
|---|---|
| Runtime | Node.js |
| Language | TypeScript |
| Framework | Fastify v5 |
| Database | PostgreSQL |
| ORM | Drizzle ORM |
| Cache/Session | Redis (ioredis) |
| Validation | Zod |
| Authentication | JWT (jose) |
| Password Hashing | bcrypt |
| Testing | Vitest + supertest |
| Package Manager | pnpm |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Layer β
β (Frontend / Mobile / Postman) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Gateway Layer β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β Rate Limiterβ β CORS/Helmet β β Cookie Parser β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Routing Layer β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β Auth Routes β β Application β β Stats Routes β β
β β /v1/auth/* β β Routes β β /v1/stats β β
β β β β /v1/apps/* β β β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Middleware Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Authentication Middleware β β
β β (JWT Verification + User Resolution) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Controller Layer β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β Auth β β Application β β Stats β β
β β Controller β β Controller β β Controller β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Service Layer β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β Auth β β Application β β Activity β β
β β Service β β Service β β Service β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Data Access Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Drizzle ORM β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββ ββββββββββββββββ β
β β PostgreSQL β β Redis β β
β β (Primary) β β (Cache/ β β
β β β β Blacklist) β β
β βββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Layered Architecture: Clear separation of concerns across routing, controllers, services, and data access layers
- Dependency Injection: Fastify's plugin system for modular component registration
- State Machine Pattern: Enforced status transitions for job applications
- Repository Pattern: Database operations abstracted through Drizzle ORM
- Middleware Chain: Authentication, rate limiting, and error handling as composable middleware
job-tracker-api/
βββ src/
β βββ db/
β β βββ index.ts # Database connection & Drizzle instance
β β βββ schema.ts # Database table definitions
β β βββ migrations/ # Generated migration files
β β
β βββ lib/
β β βββ response.ts # Standardized response builders
β β
β βββ middleware/
β β βββ auth.middleware.ts # JWT authentication middleware
β β
β βββ modules/
β β βββ auth/
β β β βββ auth.controller.ts
β β β βββ auth.service.ts
β β β βββ auth.routes.ts
β β β βββ auth.schema.ts
β β β
β β βββ applications/
β β β βββ application.controller.ts
β β β βββ applications.service.ts
β β β βββ applications.routes.ts
β β β βββ application.schema.ts
β β β βββ status-machine.ts # State machine for status transitions
β β β
β β βββ activity/
β β β βββ activity.service.ts # Activity logging service
β β β
β β βββ stats/
β β βββ stats.routes.ts # Statistics endpoints
β β
β βββ plugins/
β β βββ error-handler.ts # Global error handler plugin
β β βββ rate-limit.ts # Rate limiting configuration
β β
β βββ schemas/
β β βββ schema.ts # Zod validation schemas
β β
β βββ types/
β β βββ fastify.d.ts # TypeScript type augmentations
β β
β βββ utils/
β β βββ auth/
β β β βββ token.ts # JWT token generation & verification
β β β
β β βββ errors/
β β βββ base.error.ts
β β βββ error.handler.ts
β β βββ error.types.ts
β β βββ error.utils.ts
β β βββ http.errors.ts
β β
β βββ server.ts # Application entry point
β βββ server.test.ts # Integration tests
β
βββ .env.example # Environment variable template
βββ drizzle.config.ts # Drizzle ORM configuration
βββ openapi.yaml # OpenAPI 3.0 specification
βββ package.json # Dependencies & scripts
βββ postman-collection.json # Postman API collection
βββ tsconfig.json # TypeScript configuration
βββ vitest.config.ts # Vitest testing configuration
| Column | Type | Constraints |
|---|---|---|
| id | UUID | PRIMARY KEY, DEFAULT RANDOM |
| TEXT | UNIQUE, NOT NULL | |
| password_hash | TEXT | NOT NULL |
| name | TEXT | NOT NULL |
| created_at | TIMESTAMP | DEFAULT NOW |
| updated_at | TIMESTAMP | DEFAULT NOW |
| Column | Type | Constraints |
|---|---|---|
| id | UUID | PRIMARY KEY, DEFAULT RANDOM |
| user_id | UUID | FOREIGN KEY β users.id (CASCADE DELETE) |
| company_name | TEXT | NOT NULL |
| role_title | TEXT | NOT NULL |
| status | ENUM | DEFAULT 'APPLIED' |
| location | TEXT | NULLABLE |
| job_url | TEXT | NULLABLE |
| salary_min | INTEGER | NULLABLE |
| salary_max | INTEGER | NULLABLE |
| notes | TEXT | NULLABLE |
| applied_at | TIMESTAMP | DEFAULT NOW |
| created_at | TIMESTAMP | DEFAULT NOW |
| updated_at | TIMESTAMP | DEFAULT NOW |
| Column | Type | Constraints |
|---|---|---|
| id | UUID | PRIMARY KEY, DEFAULT RANDOM |
| application_id | UUID | FOREIGN KEY β applications.id (CASCADE DELETE) |
| from_status | ENUM | NULLABLE |
| to_status | ENUM | NOT NULL |
| note | TEXT | NULLABLE |
| created_at | TIMESTAMP | DEFAULT NOW |
| Column | Type | Constraints |
|---|---|---|
| id | UUID | PRIMARY KEY, DEFAULT RANDOM |
| user_id | UUID | FOREIGN KEY β users.id (CASCADE DELETE) |
| token | TEXT | UNIQUE, NOT NULL |
| expires_at | TIMESTAMP | NOT NULL |
| created_at | TIMESTAMP | DEFAULT NOW |
APPLIED β SCREENING β INTERVIEW β OFFER β ACCEPTED
β β β β
ββββββ REJECTED ββββββ΄βββββββββββ
β
WITHDRAWN
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /v1/auth/register |
Register new user | β |
| POST | /v1/auth/login |
Login user | β |
| GET | /v1/auth/me |
Get current user | β |
| DELETE | /v1/auth/logout |
Logout user | β |
| POST | /v1/auth/refresh |
Refresh access token | β |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /v1/applications |
Create application | β |
| GET | /v1/applications |
List all applications | β |
| GET | /v1/applications/:id |
Get single application | β |
| GET | /v1/applications/:id/activity |
Get activity log | β |
| PATCH | /v1/applications/:id |
Update application | β |
| PATCH | /v1/applications/:id/status |
Update status | β |
| DELETE | /v1/applications/:id |
Delete application | β |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /v1/stats |
Get application statistics | β |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /health |
Health check | β |
The application uses a dual-token JWT system:
-
Access Token (Short-lived: 15 minutes)
- Stored in HTTP-only cookie
- Used for API authentication
- Supports revocation via Redis blacklist
-
Refresh Token (Long-lived: 7 days)
- Stored securely server-side
- Used to obtain new access tokens
- JTI (JWT ID) stored in database for revocation
- Password Hashing: bcrypt with configurable salt rounds (default: 12)
- Token Blacklisting: Revoked tokens stored in Redis
- Rate Limiting:
- Global: 100 requests/minute
- Strict (auth): 5 requests/minute
- Per-route customization
- Security Headers: Helmet.js for XSS, content-type sniffing protection
- CORS: Configurable cross-origin resource sharing
- Input Validation: Zod schemas for all request bodies
- Type Safety: Full TypeScript coverage with strict mode
{
sub: string; // User ID
email: string; // User email
type: "access" | "refresh";
iat: number; // Issued at
exp: number; // Expiration
iss: string; // Issuer
aud: string; // Audience
jti: string; // JWT ID (for revocation)
}The application implements a state machine to enforce valid status transitions:
βββββββββββββββ
β APPLIED β
ββββββββ¬βββββββ
β
βββββββββββββββββββΌββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββββ
β SCREENING β β REJECTED β β WITHDRAWN β
βββββββ¬ββββββ ββββββ¬βββββββ βββββββββββββββ
β β
β βββββββ΄ββββββ
β β β
βΌ β β
βββββββββββββ β β
β INTERVIEW β β β
βββββββ¬ββββββ β β
β β β
ββββββ΄βββββ β β
β β β β
βΌ βΌ β β
βββββββββ ββββββββββ΄β΄ββββββββββββ
β OFFER β β
βββββ¬ββββ β
β β
βΌ β
βββββββββββ΄β
β ACCEPTED β
ββββββββββββ
| From Status | To Statuses |
|---|---|
| APPLIED | SCREENING, REJECTED, WITHDRAWN |
| SCREENING | INTERVIEW, REJECTED, WITHDRAWN |
| INTERVIEW | OFFER, REJECTED, WITHDRAWN |
| OFFER | ACCEPTED, REJECTED |
| ACCEPTED | (terminal state) |
| REJECTED | (terminal state) |
| WITHDRAWN | (terminal state) |
Invalid transitions throw an error and are logged.
- Node.js v18+
- pnpm v8+
- PostgreSQL v14+
- Redis v6+
-
Clone the repository
git clone <repository-url> cd job-tracker-api
-
Install dependencies
pnpm install
-
Set up environment variables
cp .env.example .env # Edit .env with your configuration -
Start Redis and PostgreSQL
# Using Docker (recommended) docker run -d --name redis -p 6379:6379 redis:latest docker run -d --name postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=job_tracker -p 5432:5432 postgres:latest -
Run database migrations
pnpm db:push # or pnpm db:generate && pnpm db:migrate
-
Start the development server
pnpm dev
The API will be available at http://localhost:3000
| Variable | Description | Default | Required |
|---|---|---|---|
DATABASE_URL |
PostgreSQL connection string | - | β |
REDIS_URL |
Redis connection string | redis://localhost:6379 |
β |
PORT |
Server port | 3000 |
β |
NODE_ENV |
Environment | development |
β |
JWT_ACCESS_SECRET |
Access token signing secret | - | β |
JWT_REFRESH_SECRET |
Refresh token signing secret | - | β |
COOKIE_SECRET |
Cookie signing secret | - | β |
JWT_ISSUER |
JWT issuer claim | your-app-name.com |
β |
JWT_AUDIENCE |
JWT audience claim | your-app-client |
β |
ACCESS_TOKEN_TTL |
Access token expiration | 15m |
β |
REFRESH_TOKEN_TTL |
Refresh token expiration | 7d |
β |
SALT_ROUNDS |
bcrypt salt rounds | 12 |
β |
DATABASE_URL=postgresql://user:password@localhost:5432/job_tracker
REDIS_URL=redis://localhost:6379
PORT=3000
NODE_ENV=development
JWT_ACCESS_SECRET=your-super-secret-access-key-min-32-chars
JWT_REFRESH_SECRET=your-super-secret-refresh-key-min-32-chars
COOKIE_SECRET=your-cookie-secret-key
JWT_ISSUER=job-tracker-api
JWT_AUDIENCE=job-tracker-client
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=7d
SALT_ROUNDS=12| Command | Description |
|---|---|
pnpm dev |
Start development server with hot reload |
pnpm build |
Compile TypeScript to JavaScript |
pnpm start |
Start production server |
pnpm test |
Run test suite |
pnpm test:watch |
Run tests in watch mode |
pnpm test:coverage |
Run tests with coverage report |
pnpm db:generate |
Generate Drizzle migrations |
pnpm db:migrate |
Run database migrations |
pnpm db:push |
Push schema directly to database |
pnpm db:studio |
Open Drizzle Studio (DB GUI) |
The project uses Vitest for testing with a comprehensive test suite covering:
- Unit tests for services and utilities
- Integration tests for API endpoints
- Middleware tests
- Error handler tests
# Run all tests
pnpm test
# Watch mode
pnpm test:watch
# With coverage
pnpm test:coverageTests are co-located with source files using the .test.ts suffix:
src/modules/auth/auth.service.test.tssrc/modules/applications/application.controller.test.tssrc/middleware/auth.middleware.test.tssrc/server.test.ts
The API is fully documented using OpenAPI 3.0. View the specification:
- File:
openapi.yaml - Swagger UI: Import the YAML file into Swagger Editor or any OpenAPI-compatible viewer
A ready-to-use Postman collection is available:
- File:
postman-collection.json - Import directly into Postman for easy API testing
-
Register a new user
curl -X POST http://localhost:3000/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"name":"John Doe","email":"john@example.com","password":"securepassword123"}'
-
Login
curl -X POST http://localhost:3000/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"john@example.com","password":"securepassword123"}'
-
Create an application (requires auth cookie from login)
curl -X POST http://localhost:3000/v1/applications \ -H "Content-Type: application/json" \ -H "Cookie: token=<access_token>" \ -d '{"companyName":"Acme Corp","roleTitle":"Software Engineer","location":"Remote"}'
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- TypeScript strict mode enabled
- ESLint configuration for code consistency
- Prettier for code formatting
- Meaningful commit messages following conventional commits
ISC
- Fastify - Blazing fast web framework
- Drizzle ORM - TypeScript ORM
- Vitest - Next-gen testing framework
- Zod - TypeScript-first schema validation
Built with β€οΈ using Fastify and TypeScript