Skip to content

codewithnuh/job-tracker-api

Repository files navigation

Job Tracker API

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.

πŸ“‹ Table of Contents


✨ Features

  • πŸ” 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

πŸ›  Tech Stack

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

πŸ— Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         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) β”‚            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Architecture Patterns

  • 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

πŸ“ Project Structure

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

πŸ—„ Database Schema

Tables

users

Column Type Constraints
id UUID PRIMARY KEY, DEFAULT RANDOM
email TEXT UNIQUE, NOT NULL
password_hash TEXT NOT NULL
name TEXT NOT NULL
created_at TIMESTAMP DEFAULT NOW
updated_at TIMESTAMP DEFAULT NOW

applications

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

activity_logs

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

refresh_tokens

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

Enums

application_status

APPLIED β†’ SCREENING β†’ INTERVIEW β†’ OFFER β†’ ACCEPTED
    ↓         ↓           ↓          ↓
    └────→ REJECTED β†β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
         WITHDRAWN

🌐 API Endpoints

Authentication

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 ❌

Applications

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 βœ…

Statistics

Method Endpoint Description Auth Required
GET /v1/stats Get application statistics βœ…

Health Check

Method Endpoint Description Auth Required
GET /health Health check ❌

πŸ” Authentication & Security

Token Strategy

The application uses a dual-token JWT system:

  1. Access Token (Short-lived: 15 minutes)

    • Stored in HTTP-only cookie
    • Used for API authentication
    • Supports revocation via Redis blacklist
  2. Refresh Token (Long-lived: 7 days)

    • Stored securely server-side
    • Used to obtain new access tokens
    • JTI (JWT ID) stored in database for revocation

Security Features

  • 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

JWT Payload Structure

{
  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)
}

πŸ”„ Application Status Workflow

The application implements a state machine to enforce valid status transitions:

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   APPLIED   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                 β”‚                 β”‚
         β–Ό                 β–Ό                 β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ SCREENING β”‚    β”‚  REJECTED β”‚    β”‚  WITHDRAWN  β”‚
   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚               β”‚
         β”‚         β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
         β”‚         β”‚           β”‚
         β–Ό         β”‚           β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚           β”‚
   β”‚ INTERVIEW β”‚   β”‚           β”‚
   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β”‚           β”‚
         β”‚         β”‚           β”‚
    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”    β”‚           β”‚
    β”‚         β”‚    β”‚           β”‚
    β–Ό         β–Ό    β”‚           β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ OFFER β”‚ β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”˜ β”‚
    β”‚     β”‚
    β–Ό     β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”˜
β”‚ ACCEPTED β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Valid Transitions

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.


πŸš€ Getting Started

Prerequisites

  • Node.js v18+
  • pnpm v8+
  • PostgreSQL v14+
  • Redis v6+

Installation

  1. Clone the repository

    git clone <repository-url>
    cd job-tracker-api
  2. Install dependencies

    pnpm install
  3. Set up environment variables

    cp .env.example .env
    # Edit .env with your configuration
  4. 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
  5. Run database migrations

    pnpm db:push
    # or
    pnpm db:generate && pnpm db:migrate
  6. Start the development server

    pnpm dev

The API will be available at http://localhost:3000


βš™οΈ Environment Variables

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 ❌

Example .env File

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

πŸ“¦ Available Scripts

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)

πŸ§ͺ Testing

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 Tests

# Run all tests
pnpm test

# Watch mode
pnpm test:watch

# With coverage
pnpm test:coverage

Test Files Location

Tests are co-located with source files using the .test.ts suffix:

  • src/modules/auth/auth.service.test.ts
  • src/modules/applications/application.controller.test.ts
  • src/middleware/auth.middleware.test.ts
  • src/server.test.ts

πŸ“– API Documentation

OpenAPI Specification

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

Postman Collection

A ready-to-use Postman collection is available:

Quick Start Guide

  1. 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"}'
  2. Login

    curl -X POST http://localhost:3000/v1/auth/login \
      -H "Content-Type: application/json" \
      -d '{"email":"john@example.com","password":"securepassword123"}'
  3. 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"}'

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Code Style

  • TypeScript strict mode enabled
  • ESLint configuration for code consistency
  • Prettier for code formatting
  • Meaningful commit messages following conventional commits

πŸ“„ License

ISC


πŸ™ Acknowledgments

  • 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

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors