DEV Community

Ursula Okafo
Ursula Okafo

Posted on

Building and Deploying a Django REST API on AWS EC2: A Complete Guide

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

  1. The Project Overview
  2. Local Development Setup
  3. Understanding the Code
  4. Deploying to AWS EC2
  5. Testing and Verification
  6. Key Learnings
  7. 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
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up Virtual Environment

python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

Step 6: Create .env File

EMAIL=your-email@example.com
NAME=Your Full Name
BACKEND_STACK=Python/Django
CAT_API_TIMEOUT=5
DEBUG=True
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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'),
]
Enter fullscreen mode Exit fullscreen mode

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')),
]
Enter fullscreen mode Exit fullscreen mode

Test Locally

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

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..."
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Sign in to AWS Console
  2. Navigate to EC2 Dashboard
  3. Click "Launch Instance"
  4. Select Ubuntu 22.04 LTS
  5. Instance type: t2.micro (free tier)
  6. Storage: 30GB (default)
  7. Security Group:
    • SSH (22) - from your IP
    • HTTP (80) - from anywhere
    • HTTPS (443) - from anywhere
  8. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 4: Clone Your Repository

cd /home/ubuntu
git clone https://github.com/YOUR_USERNAME/backend-wizards-stage0.git
cd backend-wizards-stage0
Enter fullscreen mode Exit fullscreen mode

Step 5: Set Up Python Environment

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install gunicorn
Enter fullscreen mode Exit fullscreen mode

Step 6: Configure Environment Variables

nano .env
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Never leave DEBUG=True in production! It exposes sensitive information.

Step 8: Create Systemd Service

sudo nano /etc/systemd/system/django.service
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 10: Configure Nginx

sudo nano /etc/nginx/sites-available/default
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 11: Start Nginx

sudo nginx -t
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check Response Headers

curl -i http://your-ec2-public-ip/me
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

6. Timeouts Prevent Hanging

Always set timeouts on external API calls:

requests.get(url, timeout=5)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 in INSTALLED_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)