A robust REST API built with Express.js and TypeScript that provides comprehensive country information, currency data, and exchange rate management. The API fetches data from external sources, stores it in a MySQL database using Prisma ORM, and offers efficient querying, filtering, and visualization capabilities.
- Features
- Architecture
- Prerequisites
- Installation & Setup
- Running the API
- API Endpoints
- Request Examples
- Response Examples
- Error Handling
- Database Schema
- Project Structure
- Technologies Used
- 🌍 Country Data Management: Fetch and manage country information including capital, region, population, and flag URLs
- 💱 Exchange Rate Integration: Real-time currency exchange rates with estimated GDP calculations
- 📊 Data Visualization: Auto-generate PNG summary images with top countries by estimated GDP
- 🔄 Data Refresh: Manually trigger data refresh from external APIs (REST Countries, ER-API)
- 🗄️ Database Persistence: MySQL database with Prisma ORM for reliable data storage
- ⚡ Rate Limiting: Built-in rate limiting to prevent API abuse
- 📝 Comprehensive Logging: Request logging middleware for monitoring
- ✅ TypeScript: Full type safety and better development experience
- 🛡️ Error Handling: Centralized error handling with meaningful error messages
- 🔍 Advanced Filtering: Query countries by region, currency, and sort by GDP
┌─────────────────────────────────────────────────────┐
│ Client / Frontend Application │
└──────────────────────────────┬──────────────────────┘
│ HTTP Requests
▼
┌─────────────────────────────────────────────────────┐
│ Express.js REST API Server (Port 3000) │
│ ┌────────────────────────────────────────────────┐ │
│ │ Rate Limiting Middleware │ │
│ │ Logging Middleware │ │
│ │ Error Handling Middleware │ │
│ └────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Route Handlers (countryRoute.ts) │ │
│ │ ├─ POST /countries/refresh │ │
│ │ ├─ GET /countries │ │
│ │ ├─ GET /countries/:name │ │
│ │ ├─ GET /countries/image │ │
│ │ ├─ DELETE /countries/:name │ │
│ │ └─ GET /status │ │
│ └────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ├─ countryService (business logic) │ │
│ │ └─ imageService (image generation) │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────┬──────────────────┬───────────────┘
│ │
┌─────────▼────────┐ ┌──────▼─────────┐
│ MySQL Database │ │ External APIs │
│ (via Prisma) │ │ ├─ REST Ctrs │
│ ├─ Country │ │ └─ ER-API │
│ └─ Metadata │ └────────────────┘
└──────────────────┘- Controllers (
src/controllers/): Handle HTTP requests/responses - Services (
src/services/): Business logic and data processing - Database (
prisma/): ORM schema and migrations - Middlewares (
src/middlewares/): Cross-cutting concerns (logging, errors) - Utils (
src/utils/): Utilities (rate limiting, API error class) - Routes (
src/routes/): Endpoint definitions
- Node.js 16+ and npm 7+
- MySQL 8.0+ running locally or remote server
- Prisma CLI (installed via npm)
cd "path/to/stage 2 country currency and exchange api"npm installThis installs all required packages:
express: Web frameworkaxios: HTTP client for external APIs@prisma/client: ORM clientcanvas: PNG image generationexpress-rate-limit: Rate limiting middlewaredotenv: Environment variable management- Development tools: TypeScript, ts-node, nodemon, eslint, prettier
Create a .env file in the project root with the following variables:
# Server Configuration
NODE_ENV=development
PORT=3000
# Database Configuration
DATABASE_URL=mysql://username:password@localhost:3306/country_exchange_db
# External API URLs
COUNTRIES_API_URL=https://restcountries.com/v2/all?fields=name,capital,region,population,flag,currencies
EXCHANGE_RATES_API_URL=https://open.er-api.com/v6/latest/USD
# Cache Configuration
CACHE_DIR=./cache
# Optional: Timeout for external API calls (milliseconds)
EXTERNAL_TIMEOUT_MS=10000Environment Variables Explanation:
NODE_ENV: Application environment (development/production)PORT: Server port (default 3000)DATABASE_URL: MySQL connection stringCOUNTRIES_API_URL: REST Countries API endpointEXCHANGE_RATES_API_URL: Exchange rates API endpointCACHE_DIR: Directory for caching generated imagesEXTERNAL_TIMEOUT_MS: Timeout for external HTTP requests
# Generate Prisma Client based on schema
npm run prisma:generate
# Run migrations to create tables
npm run prisma:migrateThese commands:
- Generate the Prisma Client (TypeScript types for database)
- Create/update MySQL database tables
npm run devThis starts the server with nodemon, which automatically restarts on file changes.
# Build the project
npm run build
# Run the compiled version
npm run servenpm run devvThis uses dotenv-cli to load .env variables before starting the dev server.
Fetches fresh data from external APIs and updates the database.
POST /countries/refreshResponse (Success - 200):
{
"message": "Countries refreshed successfully",
"status": 200,
"body": {
"success": true,
"last_refreshed_at": "2025-10-25T14:30:45.123Z"
}
}Response (Error - 503):
{
"error": "External data source unavailable",
"details": "Connection timeout"
}Retrieve a list of all countries with optional filtering and sorting.
GET /countries?region=Africa¤cy=ZAR&sort=gdp_descQuery Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
region |
string | Filter by region | Africa, Europe, Americas |
currency |
string | Filter by currency code | USD, EUR, GBP |
sort |
string | Sort order | gdp_desc, gdp_asc, or default (by name) |
Response (Success - 200):
[
{
"id": 1,
"name": "South Africa",
"capital": "Pretoria",
"region": "Africa",
"population": 59308690,
"currencyCode": "ZAR",
"exchangeRate": 18.5,
"estimatedGdp": 412567890.45,
"flagUrl": "https://restcountries.com/data/zaf.svg",
"lastRefreshedAt": "2025-10-25T14:30:45.123Z"
}
]Retrieve a specific country by its name.
GET /countries/:nameURL Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Country name (case-insensitive) |
Example:
GET /countries/NigeriaResponse (Success - 200):
{
"id": 5,
"name": "Nigeria",
"capital": "Abuja",
"region": "Africa",
"population": 223804632,
"currencyCode": "NGN",
"exchangeRate": 1545.5,
"estimatedGdp": 215678945.23,
"flagUrl": "https://restcountries.com/data/nga.svg",
"lastRefreshedAt": "2025-10-25T14:30:45.123Z"
}Response (Error - 404):
{
"error": "Country not found"
}Remove a country from the database.
DELETE /countries/:nameURL Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Country name (case-insensitive) |
Example:
DELETE /countries/NigeriaResponse (Success - 200):
{
"message": "Nigeria deleted successfully"
}Response (Error - 404):
{
"error": "Country not found"
}Retrieve a PNG image with country statistics (top 5 by GDP).
GET /countries/imageResponse: PNG image file
Retrieve API health and database statistics.
GET /statusResponse (Success - 200):
{
"total_countries": 195,
"last_refreshed_at": "2025-10-25T14:30:45.123Z"
}# Get all countries
curl http://localhost:3000/countries
# Get countries in Africa sorted by GDP (descending)
curl "http://localhost:3000/countries?region=Africa&sort=gdp_desc"
# Get specific country
curl http://localhost:3000/countries/Nigeria
# Refresh data
curl -X POST http://localhost:3000/countries/refresh
# Delete country
curl -X DELETE http://localhost:3000/countries/Nigeria
# Get status
curl http://localhost:3000/status
# Get summary image
curl http://localhost:3000/countries/image -o summary.png// Get all countries
const countries = await fetch('http://localhost:3000/countries')
.then(res => res.json());
// Get countries by region with filters
const africaCountries = await fetch(
'http://localhost:3000/countries?region=Africa&sort=gdp_desc'
)
.then(res => res.json());
// Get specific country
const nigeria = await fetch('http://localhost:3000/countries/Nigeria')
.then(res => res.json());
// Refresh data
await fetch('http://localhost:3000/countries/refresh', { method: 'POST' })
.then(res => res.json());
// Delete country
await fetch('http://localhost:3000/countries/Nigeria', { method: 'DELETE' })
.then(res => res.json());import axios from 'axios';
const API = axios.create({ baseURL: 'http://localhost:3000' });
// Get all countries
const countries = await API.get('/countries');
// Refresh data
await API.post('/countries/refresh');
// Get country
const country = await API.get('/countries/Nigeria');
// Delete country
await API.delete('/countries/Nigeria');{
"data": [...],
"status": 200
}{
"error": "Error message",
"details": "Detailed error information",
"status": 400|404|500|503
}| Code | Meaning |
|---|---|
| 200 | Request successful |
| 400 | Bad request / Missing required parameter |
| 404 | Resource not found |
| 503 | External data source unavailable |
| 500 | Internal server error |
The API implements centralized error handling with the following error scenarios:
{
"error": "Validation failed",
"details": "name parameter is required",
"status": 400
}{
"error": "Country not found",
"status": 404
}{
"error": "External data source unavailable",
"details": "Connection timeout",
"status": 503
}{
"error": "Internal server error",
"status": 500
}{
"error": "Rate limit exceeded",
"details": "Too many requests from this IP",
"status": 429
}model Country {
id String @id @default(uuid())
name String @unique
capital String?
region String?
population Int
currency_code String?
exchange_rate Float?
estimated_gdp Float?
flag_url String?
last_refreshed_at DateTime
}model Metadata {
key String @id
value String?
}Stored Metadata:
last_refreshed_at: Timestamp of last successful data refresh
├── src/
│ ├── config/
│ │ ├── config.ts # Environment configuration & validation
│ │ └── db.ts # Prisma client initialization
│ ├── controllers/
│ │ └── countryController.ts # Request handlers
│ ├── services/
│ │ ├── countryService.ts # Country business logic
│ │ ├── imageService.ts # Image generation logic
│ │ └── metaService.ts # Metadata operations
│ ├── routes/
│ │ └── countryRoute.ts # API route definitions
│ ├── middlewares/
│ │ ├── errorHandler.ts # Error handling middleware
│ │ └── logging.ts # Request logging middleware
│ ├── utils/
│ │ ├── apiError.ts # Custom error class
│ │ └── rate-limiting.ts # Rate limiting configuration
│ ├── types/
│ │ └── canvas.d.ts # TypeScript declarations
│ └── index.ts # Application entry point
├── prisma/
│ ├── schema.prisma # Database schema
│ └── migrations/ # Database migration files
├── generated/
│ └── prisma/ # Generated Prisma Client
├── cache/ # Generated image cache
├── .env # Environment variables (local)
├── .env.example # Environment template
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript configuration
└── README.md # This file| Technology | Purpose |
|---|---|
| Express.js 5.x | Web framework for REST API |
| TypeScript 5.x | Type-safe JavaScript |
| Prisma 6.x | ORM for database operations |
| MySQL 8.0+ | Relational database |
| Axios 1.x | HTTP client for external APIs |
| Node Canvas 3.x | PNG image generation |
| express-rate-limit | API rate limiting |
| Dotenv | Environment variable management |
| Nodemon | Development auto-reload |
| ESLint & Prettier | Code linting & formatting |
When /countries/refresh is called:
- Fetches country data from REST Countries API
- Fetches exchange rates from ER-API
- Calculates estimated GDP:
(population × random_multiplier) / exchange_rate - Upserts (insert or update) all countries in database
- Generates and caches a summary PNG image
- Returns success status with refresh timestamp
- Prevents API abuse by limiting requests per IP
- Configured in
src/utils/rate-limiting.ts - Default: 100 requests per 15 minutes
- Creates PNG image with:
- Total countries count
- Top 5 countries by estimated GDP
- Last refresh timestamp
- Cached at
./cache/summary.png - Canvas library used for native PNG rendering
Query parameters allow flexible data retrieval:
- Filter by region:
?region=Africa - Filter by currency:
?currency=USD - Sort by GDP:
?sort=gdp_descor?sort=gdp_asc - Default sort: By country name (ascending)
- Centralized middleware catches all errors
- Custom
ApiErrorclass for structured error responses - Validation of required environment variables on startup
- External API timeouts handled gracefully
Solution: Ensure all required variables in .env are set and not empty.
Solution:
- Verify MySQL is running
- Check DATABASE_URL is correct
- Ensure database exists:
CREATE DATABASE country_exchange_db;
Solution:
- Check internet connection
- Verify external API URLs are accessible
- Increase
EXTERNAL_TIMEOUT_MSif APIs are slow
Solution:
-
Change
PORTin.envfile -
Or kill process using the port:
# Windows: netstat -ano | findstr :3000 # Linux/Mac: lsof -i :3000
ISC © ideategudy
Contributions welcome! Please ensure:
- TypeScript strict mode compliance
- Code formatted with Prettier
- ESLint rules pass
- Database migrations included for schema changes
