This project is a robust, high-performance RESTful API built for the HNG Internship Stage 2 Backend Task. The service fetches country and currency exchange rate data from external APIs, processes and enriches this data, caches it in a persistent database, and provides a suite of endpoints for CRUD operations and data retrieval.
The application is built with a focus on performance, scalability, and robustness, utilizing asynchronous programming for I/O-bound tasks and efficient bulk database operations.
Live API Documentation (Swagger UI): https://hng-stage-2-countries-zichdan7442-9x1yl5ab.leapcell.dev/
- Data Aggregation: Fetches and combines data from two separate external APIs (RestCountries and Open Exchange Rates).
- Database Caching: All processed data is stored in a persistent PostgreSQL/MySQL database to ensure fast and reliable access.
- High-Performance Refresh: The
POST /countries/refreshendpoint usesasyncioandhttpxto make concurrent API calls, and Django'sbulk_create/bulk_updatefor highly efficient database writes, all within a single atomic transaction. - Rich Filtering & Sorting: The main
GET /countriesendpoint supports filtering byregionandcurrency_code, as well as sorting by fields likeestimated_gdp. - Dynamic Image Generation: Automatically generates and serves a summary image (
/countries/image) displaying key statistics and country flags after each data refresh. - Robust Error Handling: Gracefully handles external API failures, timeouts, and database errors, returning appropriate HTTP status codes (503, 404, 400, etc.).
- Interactive API Documentation: Includes a full Swagger UI for easy exploration and testing of all endpoints.
- Backend Framework: Django & Django REST Framework
- Database: PostgreSQL / MySQL
- Asynchronous HTTP:
httpx,asyncio - Image Processing:
Pillow - Production Server: Gunicorn
- API Documentation:
drf-yasg(Swagger)
All endpoints are detailed below. You can also interact with them live via the Swagger UI.
- Description: The core endpoint. Fetches fresh data from the external APIs, processes it, and updates the local database cache. This is a long-running, high-performance task.
- Request Body: None.
- Success Response (200 OK):
{ "status": "success", "countries_processed": 250 } - Error Response (503 Service Unavailable):
{ "error": "External data source unavailable", "details": "Could not fetch data from RestCountries API" }
- Description: Retrieves a paginated list of all countries from the database.
- Query Parameters:
region(string): Filters countries by region (e.g.,?region=Africa).currency_code(string): Filters countries by currency code (e.g.,?currency_code=NGN).ordering(string): Sorts the results. Use-for descending order (e.g.,?ordering=-estimated_gdp).
- Success Response (200 OK):
[ { "id": 1, "name": "Nigeria", "capital": "Abuja", "region": "Africa", "population": 206139589, "currency_code": "NGN", "exchange_rate": "1600.2300", "estimated_gdp": "25767448125.20", "flag_url": "https://flagcdn.com/ng.svg", "last_refreshed_at": "2025-10-25T18:00:00Z" } ]
- Description: Retrieves a single country by its name.
- Success Response (200 OK): A single country object (same structure as above).
- Error Response (404 Not Found):
{ "detail": "Not found." }
- Description: Deletes a country record from the database by its name.
- Success Response:
204 No Contentwith an empty body. - Error Response (404 Not Found):
{ "detail": "Not found." }
- Description: Provides a quick status check of the data cache.
- Success Response (200 OK):
{ "total_countries": 250, "last_refreshed_at": "2025-10-25T18:00:00Z" }
- Description: Serves the dynamically generated summary image.
- Success Response (200 OK): An image file (
image/png). - Error Response (404 Not Found):
{ "error": "Summary image not found. Run the /countries/refresh endpoint first." }
Follow these steps to run the project on your local machine.
- Python 3.8+
- Git
- A running PostgreSQL or MySQL database.
git clone https://github.com/zichdan/hng-stage-2-countries.git
cd hng-stage-2-countries# Create and activate the virtual environment
python -m venv venv
source venv/bin/activate # On Windows: .\venv\Scripts\activateAll required packages are listed in requirements.txt.
pip install -r requirements.txtCreate a .env file in the project root. You can copy the provided .env.example as a template.
cp .env.example .envNow, open the .env file and add your database URL and a Django secret key.
This command will create the necessary tables in your database.
python manage.py migratepython manage.py runserverThe API will be available at http://127.0.0.1:8000/.
The following environment variables are required. They should be placed in a .env file for local development or set in your hosting provider's dashboard for production.
| Variable | Description | Example Value |
|---|---|---|
SECRET_KEY |
A long, unique secret key for Django's security features. | your-super-secret-django-key |
DEBUG |
Toggles Django's debug mode. Must be False in production. |
True |
DATABASE_URL |
The connection string for your database. | postgres://user:pass@host/db |
ALLOWED_HOSTS |
Comma-separated list of trusted domains for production. | myapp.com,127.0.0.1 |
After setting up the project locally:
- Crucial First Step: Send a
POSTrequest to thehttp://127.0.0.1:8000/countries/refreshendpoint. This will populate your database. You can do this easily from the Swagger UI. - Once the refresh is complete (it may take 10-20 seconds), you can test all the
GETendpoints to retrieve the data. - Test the
GET /countries/imageendpoint to see the generated summary image.
- Asynchronous I/O: The
refresh_country_dataservice usesasyncioandhttpxto make concurrent, non-blocking calls to the external APIs, significantly reducing the total time spent waiting for network responses. - Bulk Database Operations: Instead of inserting or updating records one by one in a loop (which is highly inefficient), the application prepares lists of new and existing countries in memory and then uses Django's
bulk_createandbulk_updateto commit them to the database in a minimal number of queries. - Data Integrity: All database write operations during a refresh are wrapped in a single
transaction.atomic()block. This ensures that the entire operation succeeds or fails as a whole, preventing partial updates and maintaining data consistency.
- Name: Daniel Ezichi Okorie