MSN Weather Wrapper API¶
FastAPI-based REST service for fetching weather data from MSN Weather.
API Request Flow¶
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD', 'primaryTextColor': '#0F172A', 'primaryBorderColor': '#1E88E5', 'lineColor': '#1E88E5', 'secondaryColor': '#BBDEFB', 'tertiaryColor': '#90CAF9', 'edgeLabelBackground': '#E3F2FD'}}}%%
sequenceDiagram
actor Client
participant API as API Gateway
participant Valid as Validation
participant Cache as LRU Cache
participant MSN as MSN Service
participant Response as Response
Client->>API: GET /api/v1/weather<br/>?city=Seattle&country=USA
activate API
API->>Valid: Sanitize input
activate Valid
Valid->>Valid: Check length < 100
Valid->>Valid: Remove bad chars
Valid->>Valid: Verify string type
Valid-->>API: โ Valid
deactivate Valid
API->>Cache: Check cache<br/>TTL bucket
activate Cache
alt Cache Hit
Cache-->>API: Return cached data
API-->>Response: 200 OK (cached)
else Cache Miss
Cache-->>API: No cache entry
deactivate Cache
API->>MSN: Fetch from MSN
activate MSN
MSN-->>API: Weather data
deactivate MSN
API->>Cache: Store in cache
API-->>Response: 200 OK (fresh)
end
deactivate API
Response-->>Client: JSON response
Interactive API Documentation¶
Swagger UI is available for interactive API exploration and testing:
- Local Development: http://localhost:5000/apidocs
- Container Deployment: the bundled Nginx config only proxies
/api/to the backend, so/apidocsand/apispec.jsonare not reachable through port 8080
The Swagger UI provides:
- ๐ Complete API documentation with request/response examples
- ๐งช Interactive endpoint testing directly from your browser
- ๐ Request/response schema validation
- ๐ท๏ธ Organized endpoints by category (health, weather)
- ๐ Session-based authentication support for recent searches
OpenAPI Specification: Available at /apispec.json for API clients and tools
API Endpoints¶
Health Check¶
Check if the API is running.
Endpoint: GET /api/v1/health
Response:
Weather by City (GET)¶
Fetch weather data for a specific location using query parameters.
Endpoint: GET /api/v1/weather
Query Parameters:
city(required): City namecountry(required): Country name
Example Request:
Success Response (200):
{
"location": {
"city": "Seattle",
"country": "USA",
"latitude": null,
"longitude": null
},
"temperature": 6.1,
"condition": "Cloudy",
"humidity": 97,
"wind_speed": 1.6
}
Error Response (400):
{
"error": "Missing required parameters",
"message": "Both 'city' and 'country' parameters are required"
}
Weather by City (POST)¶
Fetch weather data for a specific location using JSON body.
Endpoint: POST /api/v1/weather
Request Body:
Example Request:
curl -X POST http://localhost:5000/api/v1/weather \
-H "Content-Type: application/json" \
-d '{"city": "London", "country": "UK"}'
Success Response (200):
{
"location": {
"city": "London",
"country": "UK",
"latitude": null,
"longitude": null
},
"temperature": 9.4,
"condition": "Mostly sunny",
"humidity": 83,
"wind_speed": 9.7
}
Error Response (400):
{
"error": "Missing required parameters",
"message": "Both 'city' and 'country' parameters are required"
}
Error Response (502):
{
"error": "Upstream service error",
"message": "Failed to fetch weather data from the upstream service."
}
Weather by Coordinates¶
Fetch weather data using geographic coordinates with reverse geocoding.
Endpoint: GET /api/v1/weather/coordinates
Query Parameters:
lat(required): Latitude (-90 to 90)lon(required): Longitude (-180 to 180)
Example Request:
Success Response (200):
{
"location": {
"city": "Seattle",
"country": "USA",
"latitude": 47.6062,
"longitude": -122.3321
},
"temperature": 15.5,
"condition": "Partly Cloudy",
"humidity": 65,
"wind_speed": 12.5
}
Error Response (400):
Python Example:
from backend import WeatherClient
client = WeatherClient()
weather = client.get_weather_by_coordinates(47.6062, -122.3321)
print(f"Weather in {weather.location.city}: {weather.temperature}ยฐC")
Frontend Example:
// Get user's location and fetch weather
navigator.geolocation.getCurrentPosition(async (position) => {
const { latitude, longitude } = position.coords;
const response = await fetch(
`/api/v1/weather/coordinates?lat=${latitude}&lon=${longitude}`
);
const weather = await response.json();
console.log(weather);
});
Recent Searches¶
Manage recent search history (session-based, max 10 searches).
Get Recent Searches¶
Endpoint: GET /api/v1/recent-searches
Response:
{
"recent_searches": [
{"city": "Seattle", "country": "USA"},
{"city": "London", "country": "UK"},
{"city": "Tokyo", "country": "Japan"}
]
}
Clear Recent Searches¶
Endpoint: DELETE /api/v1/recent-searches
Response:
Example:
# Get recent searches
curl http://localhost:5000/api/v1/recent-searches
# Clear recent searches
curl -X DELETE http://localhost:5000/api/v1/recent-searches
Running the API¶
Development Mode¶
The API will be available at http://localhost:5000
Production Mode (Recommended)¶
The project includes Gunicorn 23.0+ as a production ASGI server. Use it for production deployments:
# Gunicorn is already included in dependencies
gunicorn -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:5000 --workers 4 --timeout 120 backend.api.main:app
Configuration:
--workers 4: Runs 4 worker processes for concurrent request handling--timeout 120: Sets 120-second timeout for slow weather API responses--bind 0.0.0.0:5000: Binds to all interfaces on port 5000
Podman Deployment: The Podman container automatically uses Gunicorn with these production settings. No additional configuration needed.
Features¶
Rate Limiting¶
- Per IP: 30 requests per minute (applies to every endpoint;
RATE_LIMIT_PER_IP) - Global limit: 200 requests per hour total (
RATE_LIMIT_GLOBAL) - Protection against DoS attacks
Caching¶
- Weather data cached for 5 minutes
- Significantly faster repeated requests
- Automatic cache invalidation
Session Management¶
- Recent searches stored per session
- Session-based (cookies)
- For production with multiple instances, consider Redis
Security¶
- Comprehensive input validation
- Protection against SQL injection, XSS, path traversal, command injection
- Rate limiting enabled
- CORS configured with credentials support
CORS & Authentication¶
CORS is enabled with credentials support for session management:
Example React Integration¶
// Fetch weather data
async function getWeather(city: string, country: string) {
const response = await fetch(
`http://localhost:5000/api/v1/weather?city=${encodeURIComponent(city)}&country=${encodeURIComponent(country)}`
);
if (!response.ok) {
throw new Error('Failed to fetch weather');
}
return await response.json();
}
// Usage
getWeather('Seattle', 'USA')
.then(data => {
console.log(`Temperature: ${data.temperature}ยฐC`);
console.log(`Condition: ${data.condition}`);
})
.catch(error => console.error(error));
Or using POST:
async function getWeather(city: string, country: string) {
const response = await fetch('http://localhost:5000/api/v1/weather', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Important for session cookies
body: JSON.stringify({ city, country }),
});
if (!response.ok) {
throw new Error('Failed to fetch weather');
}
return await response.json();
}
Using Geolocation:
async function getWeatherByLocation() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
const response = await fetch(
`/api/v1/weather/coordinates?lat=${latitude}&lon=${longitude}`,
{ credentials: 'include' }
);
const data = await response.json();
resolve(data);
},
reject
);
});
}
Testing¶
Run the API tests:
# All tests
pytest
# API tests only
pytest tests/test_api.py -v
# Integration tests (requires the app container on http://localhost:8080)
pytest tests/test_integration.py -v --override-ini="addopts="
# Security tests
pytest tests/test_security.py -v
Production Deployment¶
Environment Variables¶
# Session secret (required for production)
export APP_SECRET_KEY='your-secret-key-here'
# API configuration
export APP_ENV=production
export APP_DEBUG=0
export CORS_ORIGINS=https://your-frontend.example.com
Security Considerations¶
- HTTPS Required: Geolocation requires HTTPS in production
- Session Storage: Use Redis for multi-instance deployments
- Rate Limiting: Configure based on expected traffic
- CORS Origins: Set an explicit comma-separated origin allowlist; wildcards are rejected
- Secret Key: Provide a unique random secret at runtime; it is not generated or stored in the image
Docker/Podman¶
# Build and run
podman build -f infra/containers/Containerfile -t msn-weather-wrapper .
podman run -p 8080:80 \
-e APP_SECRET_KEY="${APP_SECRET_KEY:?Set APP_SECRET_KEY first}" \
-e CORS_ORIGINS="${CORS_ORIGINS:-}" \
msn-weather-wrapper
# Using compose
export APP_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" # required by the production compose file
podman-compose -f infra/compose/podman-compose.yml up -d
Next.js Frontend Examples¶
The frontend application in frontend/app/ provides a complete example of API integration using Next.js and TypeScript. See frontend/app/page.tsx and frontend/app/components/ for reference implementations featuring:
- Loading states and error handling
- Input validation and URL encoding
- Clean UI with weather display
Complete Endpoint Summary¶
| Method | Endpoint | Description | Version |
|---|---|---|---|
| GET | /api/v1/health |
Basic health check | v1 |
| GET | /api/v1/health/live |
Liveness probe | v1 |
| GET | /api/v1/health/ready |
Readiness probe | v1 |
| GET | /api/v1/weather |
Get weather by city | v1 |
| POST | /api/v1/weather |
Get weather by city | v1 |
| GET | /api/v1/weather/coordinates |
Get weather by coordinates | v1 |
| GET | /api/v1/recent-searches |
Get recent searches | v1 |
| DELETE | /api/v1/recent-searches |
Clear recent searches | v1 |
Error Codes¶
The API uses standard HTTP status codes and returns consistent error response format:
HTTP Status Codes¶
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid or missing parameters |
| 404 | Not Found | Location or endpoint not found |
| 415 | Unsupported Media Type | POST body is not application/json |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error occurred |
| 502 | Bad Gateway | Upstream MSN Weather request failed |
| 503 | Service Unavailable | Readiness probe reports not ready |
Error Response Format¶
All errors follow this JSON structure:
Common Error Examples¶
Missing Parameters (400):
{
"error": "Missing required parameters",
"message": "Both 'city' and 'country' parameters are required"
}
Invalid Input (400):
Rate Limit Exceeded (429):
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later.",
"retry_after": "42"
}
Location Not Found (404):
Upstream Error (502):
{
"error": "Upstream service error",
"message": "Failed to fetch weather data from the upstream service."
}
Rate Limiting Details¶
The API implements rate limiting to prevent abuse and ensure fair usage:
Limits¶
- Per IP Address: 30 requests per minute
- Total Server: 200 requests per hour
- Recent Searches: 10 searches maximum per session
Rate Limit Headers¶
A rate-limited request returns 429 with a Retry-After header (seconds until the limit window frees up). Every response also carries an X-Request-ID header:
Handling Rate Limits¶
When rate limited (429 response):
- Respect the limit: Wait before retrying
- Check Retry-After: Number of seconds to wait before retrying
- Implement backoff: Exponential backoff for retries
- Cache responses: Store results locally (5 min cache recommended)
Example with retry logic:
import time
import requests
def get_weather_with_retry(city, country, max_retries=3):
for attempt in range(max_retries):
response = requests.get(
"http://localhost:5000/api/v1/weather", params={"city": city, "country": country}
)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
wait_time = int(response.headers.get("Retry-After", 1))
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
continue
response.raise_for_status()
raise Exception("Max retries exceeded")
Response Caching¶
The API caches responses for 5 minutes to improve performance:
- Cache Key: city, country, and the current time bucket (coordinate lookups are not cached)
- Cache Duration: 300 seconds (5 minutes;
CACHE_DURATION) - Cache Hit: Returns cached data without contacting MSN Weather
- Cache Miss: Fetches fresh data from MSN Weather
Benefits:
- Reduces load on MSN Weather
- Faster responses for repeated requests
- Lower rate limit consumption
Input Validation¶
All inputs are validated for security and data integrity:
City/Country Names¶
- Allowed: Letters, spaces, hyphens, periods, commas, Unicode characters
- Max Length: 100 characters
- Blocked: Special characters
<>;&|$\ - Trimmed: Leading/trailing whitespace removed
Coordinates¶
- Latitude: -90 to 90 (decimal degrees)
- Longitude: -180 to 180 (decimal degrees)
- Format: Float or integer
Security Features¶
- โ SQL injection prevention
- โ XSS attack prevention
- โ Path traversal protection
- โ Command injection blocking
- โ Null byte filtering
- โ Input length limits
- โ Type validation
See SECURITY.md for complete security documentation.
CORS Configuration¶
Cross-Origin Resource Sharing (CORS) is enabled for web applications with full support in both development and production environments:
# CORS Headers (automatically added)
Access-Control-Allow-Origin: <configured-origin>
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Key Features:
- โ Explicit origin allowlist; wildcard origins are rejected
- โ Credentials support for session-based authentication
- โ Preflight request (OPTIONS) handling
- โ FastAPI handles CORS; Nginx does not reflect request origins
Environment Variables:
# Restrict to specific origins (required when using cross-origin requests)
CORS_ORIGINS=https://yourapp.com
# Allow multiple origins
CORS_ORIGINS=https://app1.com,https://app2.com
Leave CORS_ORIGINS unset for same-origin deployments. Do not use *; the API rejects wildcard origins because it supports credentialed sessions.
Logging¶
Structured JSON logging with request tracking:
{
"event": "request_started",
"request_id": "6f1c2f0e-3b0a-4c0e-9a51-2d0c3c1f4b7a",
"method": "GET",
"path": "/api/v1/weather",
"ip": "192.168.1.1",
"level": "info",
"timestamp": "2025-12-02T15:30:45.123456Z"
}
Log Levels:
INFO: Normal requestsWARNING: Validation errors, locations not foundERROR: Upstream failures, server errors, exceptions
Configure logging:
Logging is configured with structlog in backend/api/services.py (JSON renderer). The application does not currently read any logging-related environment variables.
See Also¶
- Security Documentation - Input validation and security
- Testing Guide - Comprehensive testing guide
- Development Guide - Local development setup