Take-Home Assignment Submission
This repository contains a complete implementation of the Currency Rate Ingestion and Analytics Service as specified in the assignment brief. All functional and non-functional requirements have been fulfilled using NestJS 11, PostgreSQL, and production-grade patterns within a constrained timebox.
- Runtime: Node.js 22
- Framework: NestJS 11
- Database: PostgreSQL (tested with Neon free tier)
- ORM: TypeORM 0.3
- HTTP Client:
@nestjs/axios - Validation:
class-validator,class-transformer - Authentication: Static API key via custom guard
- Containerization: Docker, Docker Compose
- Language: TypeScript (strict mode)
All required features from the assignment specification are fully implemented:
-
Third-Party API Integration
- Fetches USD-based exchange rates for five target currencies (INR, EUR, GBP, JPY, CAD) from
https://api.frankfurter.app - Implements 5-second timeout and retry logic with exponential backoff
- Returns HTTP 503 on persistent upstream failures
- Fetches USD-based exchange rates for five target currencies (INR, EUR, GBP, JPY, CAD) from
-
PostgreSQL Data Storage
- Uses a dedicated
exchange_ratestable with proper schema:baseCurrency,targetCurrency(VARCHAR(3))rate(DECIMAL(15,6))fetchedAt(TIMESTAMPTZ)fetchedAtMinute(INTEGER) — derived for deduplication
- Enforces uniqueness via composite constraint on
(baseCurrency, targetCurrency, fetchedAtMinute)to prevent duplicate records within the same minute window
- Uses a dedicated
-
REST API Endpoints
POST /rates/fetch: Triggers immediate ingestion and storageGET /rates/latest?base=USD: Returns the most recent rate for each target currency using a windowed query (ROW_NUMBER() OVER PARTITION BY)GET /rates/average?base=USD&target=INR&period=24h: Computes arithmetic mean over configurable time windows (1h, 6h, 12h, 24h, 7d)
-
Background Processing
- Scheduled ingestion every 3 hours using
@Cron(CronExpression.EVERY_3_HOURS) - Runs asynchronously without blocking HTTP requests
- Reuses the same ingestion logic as the manual endpoint
- Scheduled ingestion every 3 hours using
-
Observability & Reliability
/healthendpoint returns{ status: 'OK', timestamp: ISO8601 }- Structured logging with contextual metadata via custom logger
- Environment-based configuration (no hardcoded secrets)
-
Bonus: Security
- Static API Key Authentication: All endpoints (except
/health) require anX-API-Keyheader orapi_keyquery parameter - Configured via
API_KEYenvironment variable
- Static API Key Authentication: All endpoints (except
-
DevOps
- Dockerized with multi-stage
Dockerfile(Node 22, non-root user) docker-compose.ymlprovided for local PostgreSQL development
- Dockerized with multi-stage
The implementation followed a deliberate, incremental approach to manage risk and validate assumptions early:
- Foundation Setup: Initialized NestJS project with health check endpoint.
- SQLite Prototyping: Used SQLite with
synchronize: trueto rapidly validate core data flow—particularly duplicate prevention viafetchedAtMinute—without infrastructure overhead. - Frankfurter Integration: Built resilient API client with timeout, retries, and response validation.
- Core APIs: Implemented
/rates/fetchand/rates/latestagainst the validated data layer. - PostgreSQL Migration: Replaced SQLite with PostgreSQL by:
- Updating entity to use
timestamptz - Parsing
PSQL_CONNECTenvironment variable - Ensuring all queries remained database-agnostic
- Updating entity to use
- Background Job: Added cron scheduler colocated in the
currencymodule. - Security & Polish: Added API key auth, DTO validation, and structured logging.
This phased strategy ensured each layer was verified before proceeding, demonstrating engineering discipline without over-engineering.
The codebase follows clean NestJS modular architecture with feature-based organization:
src/
├── app.* # Root module and health check
├── common/
│ ├── structured-logger.ts # Context-aware logging utility
│ └── guards/api-key.guard.ts # API key authentication guard
├── config/database.config.ts # Parses PSQL_CONNECT into TypeORM config
└── currency/ # Unified domain module
├── currency.controller.ts # REST endpoints
├── currency.service.ts # Business logic
├── dto/ # Validation objects (LatestRatesDto, AverageRatesDto)
├── entities/exchange-rate.entity.ts # PostgreSQL schema
├── frankfurter/frankfurter.service.ts # Third-party API client
└── schedular/schedular.service.ts # Cron job implementation
All currency-related logic is encapsulated in the currency module, ensuring high cohesion and testability.
-
Manual Ingestion (
POST /rates/fetch)- Controller →
CurrencyService.fetchAndSaveRates('USD') - Service →
FrankfurterService.fetchLatest() - Frankfurter → Validates response → Returns rates
- Service → Saves each rate if not duplicate (using
fetchedAtMinute)
- Controller →
-
Latest Rates (
GET /rates/latest)- Controller →
CurrencyService.getLatestRates(base) - Service → Executes windowed query:
SELECT *, ROW_NUMBER() OVER ( PARTITION BY targetCurrency ORDER BY fetchedAt DESC ) AS rn FROM exchange_rates WHERE baseCurrency = ?
- Filters to
rn = 1→ Returns latest rate per target
- Controller →
-
Average Rates (
GET /rates/average)- Controller → Validates period (1h/6h/12h/24h/7d)
- Service → Computes start date → Queries
AVG(rate)over time window
-
Background Job
SchedulerServicetriggersfetchAndSaveRates('USD')every 3 hours- Same path as manual ingestion → Ensures consistency
-
Authentication
ApiKeyGuardruns on all routes except/health- Checks
X-API-Keyheader orapi_keyquery param againstAPI_KEYenv var
Prerequisites: Node.js 22, Docker, Docker Compose
-
Install dependencies:
npm install
-
Create
.env:API_KEY=your-test-key PSQL_CONNECT=postgresql://postgres:postgres@localhost/currency_db?sslmode=disable
-
Start services:
docker-compose up --build
-
Test:
curl http://localhost:3000/health curl -H "X-API-Key: your-test-key" -X POST http://localhost:3000/rates/fetch
This project is licensed under the GNU General Public License v2.0. See the LICENSE file for details.