A small FastAPI service that returns current weather for a city name. It sits in front of the OpenWeatherMap API, caches results in Redis to cut down on external calls, and rate-limits clients per IP.
https://roadmap.sh/projects/weather-api-wrapper-service
- City weather lookup — resolves a city name to coordinates, then fetches current weather.
- Two-layer caching (Redis)
- Geocoding results (city → coordinates) are cached for 30 days, since a city's coordinates essentially never change.
- Weather results are cached for 10 minutes, since weather changes often.
- This means a cache miss after the weather TTL expires but within the 30-day coordinate window costs one external API call instead of two.
- Per-IP rate limiting — a fixed-window limiter (default: 10 requests / 60 seconds per client IP) backed by Redis, returning
429 Too Many Requestsonce exceeded. - Fully async — uses
httpx.AsyncClientandredis.asynciothroughout, so a slow upstream call doesn't block the event loop for other clients.
Client
│
▼
FastAPI route (routes.py)
│ ┌────────────────────┐
├─▶│ rate_limit decorator │ → 429 if IP exceeded quota
│ └────────────────────┘
▼
service.get_city_weather()
│ ┌────────────────────┐
├─▶│ cache_response decorator │ → return cached JSON if present
│ └────────────────────┘
▼
service.get_city_coordinates() (also cached, 30-day TTL)
│
▼
OpenWeatherMap Geocoding API → OpenWeatherMap Weather API
└── Caching-Weather-API/
├── app/
│ ├── main.py # FastAPI app entrypoint
│ ├── routes.py # API routes
│ ├── service.py # OpenWeatherMap integration (geocoding + weather)
│ └── decorators.py # cache_response and rate_limit decorators, Redis client
├── tests/
│ ├── test_concurency.py # fires concurrent requests to check caching / async behavior
│ └── test_ratelimit.py # fires repeated requests to check rate limiting
├── README.md
├── requirements.txt
├── .env
└── .gitignorecache_response(ttl, prefix) in decorators.py wraps an async function. On each call it:
- Builds a Redis key from
prefix, the function name, and the call arguments (hashed with SHA-256). - Returns the cached JSON value from Redis if the key exists (cache hit — no external call is made).
- Otherwise calls the wrapped function, stores its result in Redis with the given
ttl, and returns it (cache miss).
Only non-None results are cached, so failed lookups (e.g. an unknown city) aren't cached and don't get stuck serving an error to everyone for the TTL duration.
rate_limit(max_requests, window_seconds) in decorators.py wraps an async route. On each call it:
- Builds a Redis key from the client's IP and the route name.
- Atomically increments a counter for that key (
INCR). - On the first request in a window, sets the key to expire after
window_seconds(EXPIRE) — this is what resets the counter. - If the counter exceeds
max_requestsbefore the window expires, raisesHTTPException(429)with the number of seconds remaining until the window resets.
Health check.
Response 200
{ "message": "successful" }Returns current weather for the given city.
Path parameters
| Name | Type | Description |
|---|---|---|
city_name |
string | Name of the city, e.g. Kyiv, Lviv |
Response 200 (success)
{
"coord": { "lon": 30.5238, "lat": 50.4547 },
"weather": [ { "main": "Clouds", "description": "overcast clouds" } ],
"main": { "temp": 21.3, "feels_like": 20.9, "humidity": 60 },
"name": "Kyiv"
}Full response shape matches OpenWeatherMap's Current Weather API.
Response 200 (city not found — OpenWeatherMap's geocoding quirk returns 200 with an empty result rather than 404)
{ "error": "Could not fetch weather for 'Atlantis'" }Response 429 (rate limit exceeded)
{ "detail": "Too many requests. Try again in 42 seconds." }- Python 3.10+
- Redis running locally (default:
localhost:6379) - An OpenWeatherMap API key (free tier is sufficient)
-
Clone the repository and enter the project folder
git clone <your-repo-url> cd <project-folder>
-
Create and activate a virtual environment
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate
-
Install dependencies
pip install -r requirements.txt
-
Configure environment variables
Create a
.envfile in the project root:OpenWeatherMap_API_key=your_api_key_here
-
Start Redis (if not already running)
redis-server
Verify it's up:
redis-cli ping # should return: PONG -
Run the API
uvicorn main:app --reload
The API will be available at
http://localhost:8000.
curl http://localhost:8000/api/city/KyivTwo standalone scripts are included for manual verification (not a formal test suite):
-
test_concurency.py— fires 5 concurrent requests for different cities and prints per-request timing plus total wall time. Useful for confirming requests aren't being serialized and that caching is reducing response time on repeat calls.python test_concurency.py
-
test_ratelimit.py— repeatedly requests the same city every 0.5s in a loop to confirm the rate limiter kicks in with429once the quota is exceeded. Stop withCtrl+C.python test_ratelimit.py
redis-cli KEYS "weather:*" # cached weather results
redis-cli KEYS "coords:*" # cached coordinate lookups
redis-cli KEYS "ratelimit:*" # active rate-limit counters
redis-cli TTL <key> # seconds remaining before a key expires
redis-cli FLUSHALL # clear all cached/rate-limit data (dev only)- Rate limiting and caching share the same Redis instance/database with no key-space separation beyond prefixes — fine for a single-service setup, worth revisiting if this ever runs alongside other apps on the same Redis.
- Rate limiting is keyed by IP only; clients behind a shared IP (NAT, corporate network) share a quota.
- No authentication — the API is fully open. Add an API-key layer before deploying publicly.