Skip to content

API Development Best Practices

This document establishes production-ready API development patterns for geospatial systems, covering FastAPI implementation, authentication, rate limiting, and comprehensive error handling.

FastAPI Geospatial API

Production-Ready API

from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import geopandas as gpd
from shapely.geometry import Point, Polygon
import uvicorn
import logging
from contextlib import asynccontextmanager

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Pydantic models
class SpatialQuery(BaseModel):
    geometry: Dict[str, Any] = Field(..., description="GeoJSON geometry")
    buffer_distance: Optional[float] = Field(None, ge=0, description="Buffer distance in meters")
    attributes: Optional[List[str]] = Field(None, description="Attributes to return")
    spatial_operation: str = Field("intersects", description="Spatial operation to perform")

class SpatialResponse(BaseModel):
    results: List[Dict[str, Any]]
    total_count: int
    processing_time: float
    spatial_bounds: Dict[str, float]

class HealthCheck(BaseModel):
    status: str
    timestamp: str
    version: str
    database_status: str

# Application lifespan
@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    logger.info("Starting Geospatial API")
    # Initialize database connections, load spatial data, etc.
    yield
    # Shutdown
    logger.info("Shutting down Geospatial API")

# Create FastAPI app
app = FastAPI(
    title="Geospatial Systems API",
    description="Production-ready geospatial API for spatial analysis and data processing",
    version="1.0.0",
    lifespan=lifespan
)

# Add middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://sempervent.github.io"],
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=["sempervent.github.io", "localhost"]
)

# Dependency injection
async def get_database():
    """Database connection dependency"""
    # Implementation for database connection
    pass

# API endpoints
@app.get("/health", response_model=HealthCheck)
async def health_check():
    """Health check endpoint"""
    return HealthCheck(
        status="healthy",
        timestamp=pd.Timestamp.now().isoformat(),
        version="1.0.0",
        database_status="connected"
    )

@app.post("/spatial/query", response_model=SpatialResponse)
async def spatial_query(
    query: SpatialQuery,
    background_tasks: BackgroundTasks,
    db=Depends(get_database)
):
    """
    Perform spatial query with comprehensive error handling
    """
    try:
        start_time = time.time()

        # Validate geometry
        if not validate_geometry(query.geometry):
            raise HTTPException(status_code=400, detail="Invalid geometry")

        # Perform spatial query
        results = await perform_spatial_query(query, db)

        # Calculate processing time
        processing_time = time.time() - start_time

        # Log query for analytics
        background_tasks.add_task(log_spatial_query, query, processing_time)

        return SpatialResponse(
            results=results,
            total_count=len(results),
            processing_time=processing_time,
            spatial_bounds=calculate_bounds(results)
        )

    except Exception as e:
        logger.error(f"Spatial query error: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")

@app.get("/spatial/bounds")
async def get_spatial_bounds(db=Depends(get_database)):
    """Get spatial bounds of all data"""
    try:
        bounds = await get_data_bounds(db)
        return {"bounds": bounds}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# Helper functions
def validate_geometry(geometry: Dict[str, Any]) -> bool:
    """Validate GeoJSON geometry"""
    try:
        from shapely.geometry import shape
        shape(geometry)
        return True
    except:
        return False

async def perform_spatial_query(query: SpatialQuery, db) -> List[Dict[str, Any]]:
    """Perform the actual spatial query"""
    # Implementation for spatial query
    pass

def calculate_bounds(results: List[Dict[str, Any]]) -> Dict[str, float]:
    """Calculate spatial bounds of results"""
    # Implementation for bounds calculation
    pass

async def log_spatial_query(query: SpatialQuery, processing_time: float):
    """Log query for analytics"""
    logger.info(f"Query processed in {processing_time:.3f}s: {query.spatial_operation}")

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        reload=True,
        log_level="info"
    )

Why: FastAPI provides automatic OpenAPI documentation, type validation, and async support. Middleware ensures security and CORS handling for production deployments.

Authentication and Authorization

JWT Authentication

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional

# Security configuration
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()

class User:
    def __init__(self, username: str, email: str, hashed_password: str):
        self.username = username
        self.email = email
        self.hashed_password = hashed_password

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verify a password against its hash"""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """Hash a password"""
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    """Create JWT access token"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """Get current authenticated user"""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

    try:
        payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    # In production, fetch user from database
    user = get_user(username)
    if user is None:
        raise credentials_exception
    return user

# Protected endpoint example
@app.get("/spatial/protected")
async def protected_spatial_query(
    current_user: User = Depends(get_current_user)
):
    """Protected spatial query endpoint"""
    return {"message": f"Hello {current_user.username}, this is a protected endpoint"}

Why: JWT authentication provides stateless, scalable authentication. Password hashing with bcrypt ensures secure credential storage.

Role-Based Access Control

from enum import Enum
from typing import List

class UserRole(str, Enum):
    ADMIN = "admin"
    ANALYST = "analyst"
    VIEWER = "viewer"

class Permission(str, Enum):
    READ_SPATIAL = "read:spatial"
    WRITE_SPATIAL = "write:spatial"
    DELETE_SPATIAL = "delete:spatial"
    ADMIN_ACCESS = "admin:access"

# Role permissions mapping
ROLE_PERMISSIONS = {
    UserRole.ADMIN: [Permission.READ_SPATIAL, Permission.WRITE_SPATIAL, Permission.DELETE_SPATIAL, Permission.ADMIN_ACCESS],
    UserRole.ANALYST: [Permission.READ_SPATIAL, Permission.WRITE_SPATIAL],
    UserRole.VIEWER: [Permission.READ_SPATIAL]
}

def require_permission(permission: Permission):
    """Decorator to require specific permission"""
    def decorator(func):
        async def wrapper(*args, **kwargs):
            current_user = kwargs.get('current_user')
            if not current_user:
                raise HTTPException(status_code=401, detail="Authentication required")

            user_permissions = ROLE_PERMISSIONS.get(current_user.role, [])
            if permission not in user_permissions:
                raise HTTPException(status_code=403, detail="Insufficient permissions")

            return await func(*args, **kwargs)
        return wrapper
    return decorator

# Usage example
@app.post("/spatial/data")
@require_permission(Permission.WRITE_SPATIAL)
async def create_spatial_data(
    data: SpatialQuery,
    current_user: User = Depends(get_current_user)
):
    """Create spatial data - requires write permission"""
    # Implementation
    pass

Why: Role-based access control provides fine-grained permission management. Decorators enable clean, reusable authorization logic.

Rate Limiting and Throttling

Redis-based Rate Limiting

import redis
import time
from fastapi import Request, HTTPException
from typing import Optional

class RateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    async def is_allowed(self, key: str, limit: int, window: int) -> bool:
        """
        Check if request is allowed based on rate limit
        """
        current_time = int(time.time())
        window_start = current_time - window

        # Remove old entries
        self.redis.zremrangebyscore(key, 0, window_start)

        # Count current requests
        current_requests = self.redis.zcard(key)

        if current_requests >= limit:
            return False

        # Add current request
        self.redis.zadd(key, {str(current_time): current_time})
        self.redis.expire(key, window)

        return True

# Rate limiting dependency
async def rate_limit_dependency(request: Request):
    """Rate limiting dependency"""
    client_ip = request.client.host
    rate_limiter = RateLimiter(redis_client)

    # Different limits for different endpoints
    if request.url.path.startswith("/spatial/query"):
        limit = 100  # 100 requests
        window = 3600  # per hour
    else:
        limit = 1000  # 1000 requests
        window = 3600  # per hour

    key = f"rate_limit:{client_ip}:{request.url.path}"

    if not await rate_limiter.is_allowed(key, limit, window):
        raise HTTPException(
            status_code=429,
            detail="Rate limit exceeded. Please try again later."
        )

# Apply rate limiting to endpoints
@app.post("/spatial/query")
async def spatial_query_with_rate_limit(
    query: SpatialQuery,
    request: Request,
    _: None = Depends(rate_limit_dependency)
):
    """Spatial query with rate limiting"""
    # Implementation
    pass

Why: Rate limiting prevents API abuse and ensures fair resource usage. Redis provides distributed rate limiting across multiple API instances.

API Documentation and Testing

Comprehensive API Testing

import pytest
from fastapi.testclient import TestClient
from unittest.mock import Mock, patch
import json

@pytest.fixture
def client():
    """Test client fixture"""
    from main import app
    return TestClient(app)

@pytest.fixture
def sample_spatial_query():
    """Sample spatial query fixture"""
    return {
        "geometry": {
            "type": "Point",
            "coordinates": [0, 0]
        },
        "buffer_distance": 1000,
        "spatial_operation": "intersects"
    }

class TestSpatialAPI:
    """Test suite for spatial API endpoints"""

    def test_health_check(self, client):
        """Test health check endpoint"""
        response = client.get("/health")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "healthy"
        assert "timestamp" in data
        assert "version" in data

    def test_spatial_query_valid(self, client, sample_spatial_query):
        """Test valid spatial query"""
        with patch('main.perform_spatial_query') as mock_query:
            mock_query.return_value = [
                {"id": 1, "name": "Test Feature", "geometry": "POINT(0 0)"}
            ]

            response = client.post("/spatial/query", json=sample_spatial_query)

            assert response.status_code == 200
            data = response.json()
            assert "results" in data
            assert "total_count" in data
            assert "processing_time" in data

    def test_spatial_query_invalid_geometry(self, client):
        """Test spatial query with invalid geometry"""
        invalid_query = {
            "geometry": {
                "type": "InvalidType",
                "coordinates": [0, 0]
            }
        }

        response = client.post("/spatial/query", json=invalid_query)

        assert response.status_code == 400
        assert "Invalid geometry" in response.json()["detail"]

    def test_spatial_query_rate_limit(self, client, sample_spatial_query):
        """Test rate limiting"""
        # Make multiple requests to trigger rate limit
        for _ in range(101):  # Exceed rate limit
            response = client.post("/spatial/query", json=sample_spatial_query)

            if response.status_code == 429:
                assert "Rate limit exceeded" in response.json()["detail"]
                break

    def test_authentication_required(self, client):
        """Test that protected endpoints require authentication"""
        response = client.get("/spatial/protected")

        assert response.status_code == 401
        assert "Could not validate credentials" in response.json()["detail"]

    def test_authorization_permissions(self, client):
        """Test role-based authorization"""
        # Test with different user roles
        admin_token = create_test_token({"sub": "admin", "role": "admin"})
        viewer_token = create_test_token({"sub": "viewer", "role": "viewer"})

        # Admin should have access
        response = client.get(
            "/spatial/protected",
            headers={"Authorization": f"Bearer {admin_token}"}
        )
        assert response.status_code == 200

        # Viewer should be denied for write operations
        response = client.post(
            "/spatial/data",
            json={"test": "data"},
            headers={"Authorization": f"Bearer {viewer_token}"}
        )
        assert response.status_code == 403

def create_test_token(payload: dict) -> str:
    """Create test JWT token"""
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

Why: Comprehensive testing ensures API reliability and security. Mocking external dependencies enables isolated testing of API logic.

API Performance Optimization

Caching Strategies

from functools import lru_cache
import redis
import json
from typing import Optional

class APICache:
    """API caching implementation"""

    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
        self.default_ttl = 3600  # 1 hour

    async def get(self, key: str) -> Optional[dict]:
        """Get cached data"""
        cached_data = self.redis.get(key)
        if cached_data:
            return json.loads(cached_data)
        return None

    async def set(self, key: str, data: dict, ttl: Optional[int] = None) -> None:
        """Set cached data"""
        ttl = ttl or self.default_ttl
        self.redis.setex(key, ttl, json.dumps(data))

    def generate_cache_key(self, endpoint: str, params: dict) -> str:
        """Generate cache key from endpoint and parameters"""
        param_str = json.dumps(params, sort_keys=True)
        return f"cache:{endpoint}:{hash(param_str)}"

# Caching decorator
def cache_response(ttl: int = 3600):
    """Decorator to cache API responses"""
    def decorator(func):
        async def wrapper(*args, **kwargs):
            cache = APICache(redis_client)

            # Generate cache key
            cache_key = cache.generate_cache_key(func.__name__, kwargs)

            # Try to get from cache
            cached_result = await cache.get(cache_key)
            if cached_result:
                return cached_result

            # Execute function and cache result
            result = await func(*args, **kwargs)
            await cache.set(cache_key, result, ttl)

            return result
        return wrapper
    return decorator

# Usage example
@app.get("/spatial/bounds")
@cache_response(ttl=1800)  # Cache for 30 minutes
async def get_spatial_bounds_cached(db=Depends(get_database)):
    """Get spatial bounds with caching"""
    bounds = await get_data_bounds(db)
    return {"bounds": bounds}

Why: Caching reduces database load and improves response times. Redis provides distributed caching across multiple API instances.

Database Connection Pooling

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
from sqlalchemy.orm import sessionmaker
from contextlib import asynccontextmanager

# Database configuration
DATABASE_URL = "postgresql://user:password@localhost/geospatial"

# Create engine with connection pooling
engine = create_engine(
    DATABASE_URL,
    poolclass=QueuePool,
    pool_size=20,
    max_overflow=30,
    pool_pre_ping=True,
    pool_recycle=3600
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@asynccontextmanager
async def get_db_session():
    """Database session context manager"""
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()

# Database dependency
async def get_database():
    """Database connection dependency with pooling"""
    async with get_db_session() as session:
        yield session

Why: Connection pooling improves database performance and resource utilization. Pre-ping ensures connection health, while pool recycling prevents stale connections.