The Inspiration
I’ve been experimenting with FastAPI, one of the most modern and performant Python frameworks for building web APIs.
This week, I decided to take it a bit further — not just build an API, but also consume another external API inside my app, add rate limiting, error handling, and a touch of developer love.
So in this post, I’ll walk you through exactly how I built a simple but structured FastAPI app that:
- Exposes routes like
/
,/health
, and/me
- Consumes an external cat-facts API 🐈
- Implements rate limiting using SlowAPI
- Handles errors gracefully
- Is ready for deployment with Procfile, requirements.txt, and pytest tests
Project Setup
Let’s start from scratch.
1. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # or on Windows: .venv\Scripts\activate
2. Install dependencies
Your requirements.txt
should look like this:
fastapi
slowapi
pydantic_settings
httpx[h2]
uvicorn[standard]
pytest
Then install:
pip install -r requirements.txt
Building the App
Let’s start with our main.py
— the heart of the app.
# main.py
from fastapi import FastAPI, Request, Response, Depends
from datetime import datetime, timezone
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from core.error_handlers import register_error_handlers
from services.http_client import safe_http_request
from core.exceptions import NotFoundException
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
from functools import lru_cache
from typing_extensions import Annotated
from core import config
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
print("App starting up...")
await startup_event()
yield
print("App shutting down...")
app = FastAPI(lifespan=lifespan, title="My Profile App")
register_error_handlers(app)
@lru_cache
def get_settings():
return config.Settings()
async def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"success": False, "error": "Too many requests, please slow down."},
)
limiter = Limiter(key_func=get_remote_address)
async def startup_event():
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
Profile_details = {
"email": "gbemilekekenny@gmail.com",
"name": "James Kehinde",
"stack": "NodeJs",
}
@app.get("/")
async def home():
return JSONResponse({
"success": True,
"message": "Welcome to my profile API"
})
@app.get("/health")
async def health_check():
return JSONResponse({
"success": True,
"message": "Ok"
})
@limiter.limit("8/minute")
@app.get("/me")
async def get_profile(request: Request, settings: Annotated[config.Settings, Depends(get_settings)]):
url = settings.cat_api_url
fact = await safe_http_request("GET", url, params={"max_length": 40}, timeout=3000)
if not fact:
raise NotFoundException("Timeout, try again later.")
data = {
"status": "success",
"user": Profile_details,
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"fact": fact["fact"]
}
return JSONResponse(content=data)
@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
return Response(status_code=204)
Key Takeaways from the Implementation
1. Rate Limiting
Using slowapi
, I restricted requests to /me
to 8 per minute per client.
This prevents abuse and adds robustness.
2. External API Consumption
I used an async HTTP client wrapper (httpx
) to fetch cat facts from an external API.
The idea was to simulate consuming a third-party service within my API.
3. Error Handling
Custom error handlers are registered in core/error_handlers.py
, ensuring consistent JSON responses instead of raw stack traces.
4. Lifespan Events
Instead of deprecated @app.on_event("startup")
, I used FastAPI’s lifespan context manager — the modern, recommended approach.
Testing the App with Pytest
To make sure everything works smoothly, I added test_main.py
:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_home_route():
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
Run tests with:
pytest -v
Running the App Locally
To start your server locally:
uvicorn main:app --reload
Your app will be live at:
👉 http://127.0.0.1:8000
You can visit:
-
/
→ Welcome route -
/health
→ Health check -
/me
→ Profile + external cat fact
🚢 Deployment Ready
If deploying on Render, Railway, or Heroku, your Procfile
should look like:
web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-8000}
Wrapping Up
In this small but practical project, I:
- Built a clean and modern FastAPI service
- Integrated an external API with async HTTP calls
- Implemented rate limiting
- Structured the app for production and deployment
This project gave me a clearer view of API consumption patterns inside FastAPI, and how easy it is to scale small ideas into something production-ready.
Author
James Kehinde — Full-Stack Developer | Node.js + FastAPI Enthusiast | Building meaningful software from Lagos 🇳🇬
💬 Let’s connect on LinkedIn or Twitter if you’re building something cool with FastAPI!
Top comments (0)