Introduction
In this article, I'll walk you through my journey of building a dynamic REST API with Django and deploying it to AWS EC2. This project is part of the HNG 13 Backend Wizards track, and it's a fantastic way to learn API development, third-party integrations, and cloud deployment.
By the end of this guide, you'll have a live API endpoint serving real data from your Django application on AWS.
What we're building: A /me
endpoint that returns your profile information along with random cat facts from an external API.
Table of Contents
- The Project Overview
- Local Development Setup
- Understanding the Code
- Deploying to AWS EC2
- Testing and Verification
- Key Learnings
- Troubleshooting Tips
Project Overview {#project-overview}
What We're Building
A simple yet powerful REST API endpoint with:
- User profile information (email, name, tech stack)
- Dynamic timestamps in ISO 8601 format
- Integration with an external Cat Facts API
- Graceful error handling with fallbacks
- Production-ready deployment on AWS
Tech Stack
- Framework: Django + Django REST Framework
- Server: Gunicorn (WSGI application server)
- Reverse Proxy: Nginx
- Hosting: AWS EC2 (Ubuntu 22.04 LTS)
- Language: Python 3.10+
Why This Stack?
- Django: Robust, batteries-included framework with excellent documentation
- DRF: Simple and powerful for building REST APIs
- Gunicorn: Reliable WSGI server for production
- Nginx: High-performance reverse proxy
- AWS: Industry-standard cloud platform with free tier
Local Development Setup {#local-development}
Step 1: Create Your Project Structure
mkdir backend-wizards-stage0
cd backend-wizards-stage0
Step 2: Set Up Virtual Environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Always use virtual environments! They isolate project dependencies and prevent conflicts.
Step 3: Install Dependencies
pip install django djangorestframework python-dotenv requests
pip freeze > requirements.txt
Why each package?
-
django
- Web framework -
djangorestframework
- Simplified API building -
python-dotenv
- Load environment variables from.env
file -
requests
- HTTP library for consuming external APIs
Step 4: Initialize Django Project
django-admin startproject config .
python manage.py startapp api
Naming convention: I use config
for the project folder to avoid naming conflicts with the django
library.
Step 5: Configure Settings
Update config/settings.py
:
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
# Your environment variables
EMAIL = os.getenv('EMAIL')
NAME = os.getenv('NAME')
BACKEND_STACK = os.getenv('BACKEND_STACK')
CAT_API_TIMEOUT = int(os.getenv('CAT_API_TIMEOUT', 5))
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # Add this
'api', # Add this
]
Step 6: Create .env
File
EMAIL=your-email@example.com
NAME=Your Full Name
BACKEND_STACK=Python/Django
CAT_API_TIMEOUT=5
DEBUG=True
Pro tip: Never commit .env
to version control. Add it to .gitignore
.
Understanding the Code {#code-walkthrough}
The Main API View
Here's the heart of the project (api/views.py
):
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status
from datetime import datetime, timezone
import requests
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
@api_view(['GET'])
def profile_endpoint(request):
"""
Returns user profile with a dynamic cat fact
"""
try:
# Fetch cat fact from external API
cat_fact = fetch_cat_fact()
# Build response
response_data = {
"status": "success",
"user": {
"email": settings.EMAIL,
"name": settings.NAME,
"stack": settings.BACKEND_STACK,
},
"timestamp": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
"fact": cat_fact,
}
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in profile endpoint: {str(e)}")
return Response(
{
"status": "error",
"message": "Failed to fetch profile data",
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def fetch_cat_fact():
"""
Fetches a random cat fact from the Cat Facts API
Returns a fallback fact if the API fails
"""
try:
response = requests.get(
'https://catfact.ninja/fact',
timeout=settings.CAT_API_TIMEOUT
)
response.raise_for_status()
return response.json()['fact']
except requests.exceptions.Timeout:
logger.warning("Cat Facts API timeout")
return "Cats have over 20 different vocal sounds!"
except requests.exceptions.RequestException as e:
logger.warning(f"Failed to fetch cat fact: {str(e)}")
return "Cats spend 70% of their lives sleeping!"
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return "A cat's purr may promote bone healing."
Key Concepts Here
1. Decorators: @api_view(['GET'])
- Restricts the endpoint to GET requests only.
2. Dynamic Timestamps:
datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')
This ensures the timestamp is always current and in ISO 8601 format.
3. Error Handling: Multiple except blocks handle different failure scenarios gracefully.
4. Timeouts: timeout=settings.CAT_API_TIMEOUT
prevents hanging indefinitely if the external API is slow.
URL Configuration
Create api/urls.py
:
from django.urls import path
from . import views
urlpatterns = [
path('me', views.profile_endpoint, name='profile'),
]
Then include it in config/urls.py
:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('api.urls')),
]
Test Locally
python manage.py runserver
Visit: http://127.0.0.1:8000/me
Expected response:
{
"status": "success",
"user": {
"email": "your-email@example.com",
"name": "Your Full Name",
"stack": "Python/Django"
},
"timestamp": "2025-10-19T14:35:22.123Z",
"fact": "Cats have a specialized collarbone..."
}
Deploying to AWS EC2 {#aws-deployment}
Why AWS EC2?
- Free tier available (t2.micro for 12 months)
- Full control over your server
- Scalable infrastructure
- Industry-standard platform
- Perfect learning tool
Step 1: Launch an EC2 Instance
- Sign in to AWS Console
- Navigate to EC2 Dashboard
- Click "Launch Instance"
- Select Ubuntu 22.04 LTS
- Instance type: t2.micro (free tier)
- Storage: 30GB (default)
- Security Group:
- SSH (22) - from your IP
- HTTP (80) - from anywhere
- HTTPS (443) - from anywhere
- Create a key pair (
.pem
file) and download it
Step 2: Connect to Your Instance
chmod 400 your-key.pem
ssh -i your-key.pem ubuntu@your-ec2-public-ip
Replace your-ec2-public-ip
with your instance's public IP from the AWS console.
Step 3: Install System Dependencies
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3-pip python3-venv git nginx
Step 4: Clone Your Repository
cd /home/ubuntu
git clone https://github.com/YOUR_USERNAME/backend-wizards-stage0.git
cd backend-wizards-stage0
Step 5: Set Up Python Environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install gunicorn
Step 6: Configure Environment Variables
nano .env
Add your variables and save (Ctrl+X, Y, Enter).
Step 7: Update Django Settings
Edit config/settings.py
:
ALLOWED_HOSTS = ['your-ec2-public-ip', 'localhost']
DEBUG = False
Never leave DEBUG=True in production! It exposes sensitive information.
Step 8: Create Systemd Service
sudo nano /etc/systemd/system/django.service
Paste:
[Unit]
Description=Django Gunicorn application
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/backend-wizards-stage0
ExecStart=/home/ubuntu/backend-wizards-stage0/venv/bin/gunicorn config.wsgi:application --bind 0.0.0.0:8000
Restart=always
[Install]
WantedBy=multi-user.target
This ensures Django restarts automatically if it crashes.
Step 9: Enable and Start Django
sudo systemctl daemon-reload
sudo systemctl enable django.service
sudo systemctl start django.service
sudo systemctl status django.service
Step 10: Configure Nginx
sudo nano /etc/nginx/sites-available/default
Replace with:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Step 11: Start Nginx
sudo nginx -t
sudo systemctl restart nginx
Step 12: Test Your API
Visit: http://your-ec2-public-ip/me
๐ Your API is now live!
Testing and Verification {#testing}
Manual Testing
curl http://your-ec2-public-ip/me
Check Response Headers
curl -i http://your-ec2-public-ip/me
Look for:
Content-Type: application/json
HTTP/1.1 200 OK
Verify Dynamic Updates
Call the endpoint multiple times and check:
- โ Timestamp changes each time
- โ Cat fact is different each time
- โ Response is consistent
Test Error Handling
Check logs to ensure errors are logged:
sudo journalctl -u django.service -f
Key Learnings {#learnings}
1. API Design Matters
Consistent response structures make client integration easier. Always version your APIs and document endpoints clearly.
2. Error Handling is Critical
Never let your API fail silently. Provide fallback responses and log everything. Users appreciate transparent error messages.
3. Environment Variables are Essential
Never hardcode credentials or configuration. Use .env
files and keep them secure.
4. Testing is Non-Negotiable
Test locally before deploying. Test from different networks. Test error scenarios.
5. Monitoring and Logging
Always log important events. Use:
logger.error("Error message")
logger.warning("Warning message")
logger.info("Info message")
6. Timeouts Prevent Hanging
Always set timeouts on external API calls:
requests.get(url, timeout=5)
7. Reverse Proxies Improve Performance
Nginx sits between users and Gunicorn, handling:
- SSL termination
- Load balancing
- Compression
- Static file serving
8. Systemd Services Ensure Availability
Using systemd services with Restart=always
ensures your API stays up even after crashes.
Troubleshooting Tips {#troubleshooting}
502 Bad Gateway
Problem: Nginx can't reach Gunicorn
Solution:
sudo systemctl status django.service
sudo journalctl -u django.service -f
Ensure Gunicorn is running on port 8000.
404 Not Found
Problem: Endpoint returns 404
Solution:
- Verify URL:
http://ip/me
(not/
or/api/me
) - Check
urls.py
routing - Ensure
api
is inINSTALLED_APPS
Environment Variables Not Loading
Problem: Settings are None
Solution:
- Verify
.env
file exists in project root - Check
.env
file syntax - Restart Django:
sudo systemctl restart django.service
Cat Facts API Timeout
Problem: API returns fallback facts consistently
Solution:
- Check internet connection
- Verify firewall isn't blocking outbound requests
- Increase timeout in
.env
:CAT_API_TIMEOUT=10
Conclusion
Building and deploying a Django API on AWS taught me valuable lessons about:
- REST API design principles
- Error handling and resilience
- Production deployment strategies
- Linux server management
- Monitoring and logging
This project serves as a solid foundation for more complex applications. From here, you can add:
- Database models
- User authentication
- Advanced caching
- Horizontal scaling
- CI/CD pipelines
Next Steps:
- Add a database (PostgreSQL)
- Implement authentication (JWT)
- Write automated tests
- Set up CI/CD with GitHub Actions
- Add API documentation (Swagger)
Resources
Let's Connect
If you found this helpful, feel free to reach out! I'm always excited to discuss backend development, cloud deployment, and web technologies.
GitHub: [Your GitHub Profile]
Twitter/X: [@Your Handle]
Email: ursulaokafo32@gmail.com
This article is part of my journey through the HNG 13 Backend Wizards track. Happy coding! ๐
Top comments (0)