Learn how to create a lightweight Web Map Service (WMS) using FastAPI. This step-by-step guide covers setting up WMS APIs, rendering map tiles, handling spatial data, and implementing OGC standards with modern Python libraries.
Creating a API server to support WMS
File Structure
app/
|- main.py
|- wms/
| |- __init__.py
| |- router.py
| |- renderer.py
Prepare a requirements.txt
fastapi[standard]==0.115.6
# Geospatial processing libraries
Pillow==10.0.1
pyproj==3.6.0
Install dependencies
pip install -r requirements.txt
router.py
Create a FastAPI router for WMS APIs.
from fastapi import APIRouter, Query, HTTPException
from starlette.responses import Response
from .renderer import render_map
router = APIRouter()
@router.get("/wms", summary="Web Map Service")
async def wms(
service: str = Query(..., description="Service type (WMS)"),
request: str = Query(..., description="Request type (GetMap)"),
layers: str = Query(..., description="Comma-separated layer names"),
bbox: str = Query(..., description="Bounding box (minx,miny,maxx,maxy)"),
width: int = Query(..., description="Map width in pixels"),
height: int = Query(..., description="Map height in pixels"),
crs: str = Query("EPSG:4326", description="Coordinate Reference System"),
format: str = Query("image/png", description="Output format"),
):
# Validate the service and request types
if service.upper() != "WMS" or request.upper() != "GETMAP":
raise HTTPException(status_code=400, detail="Invalid service or request type")
# Parse the bounding box
try:
minx, miny, maxx, maxy = map(float, bbox.split(","))
except ValueError:
raise HTTPException(status_code=400, detail="Invalid bounding box format")
# Render the map
image = render_map(layers, (minx, miny, maxx, maxy), width, height, crs)
# Return the map image
if format.lower() == "image/png":
return Response(image, media_type="image/png")
else:
raise HTTPException(status_code=400, detail="Unsupported format")
renderer.py
Use Pillow for rendering
from PIL import Image, ImageDraw
import io
def render_map(layers: str, bbox: tuple, width: int, height: int, crs: str) -> bytes:
"""
Render a map image for given parameters.
"""
minx, miny, maxx, maxy = bbox
layer_list = layers.split(",")
# Create a blank image
image = Image.new("RGBA", (width, height), "white")
draw = ImageDraw.Draw(image)
# Example: Draw bounding box (you can replace this with real data rendering)
draw.rectangle([(0, 0), (width, height)], outline="black")
draw.text((10, 10), f"Layers: {', '.join(layer_list)}", fill="black")
draw.text((10, 30), f"BBOX: {bbox}", fill="black")
draw.text((10, 50), f"CRS: {crs}", fill="black")
# Convert image to bytes
img_bytes = io.BytesIO()
image.save(img_bytes, format="PNG")
return img_bytes.getvalue()
main.py
Register the WMS router in the main FastAPI app.
from fastapi import FastAPI
from wms.router import router as wms_router
app = FastAPI(title="FastAPI WMS Server")
app.include_router(wms_router, prefix="/api")
# Test root endpoint
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI WMS Server"}
Testing
Run the application
fastapi dev
Access the WMS endpoint in your browser
http://127.0.0.1:8000/api/wms?service=WMS&request=GetMap&layers=test_layer&bbox=-180,-90,180,90&width=800&height=400&crs=EPSG:4326&format=image/png
The image shows at your browser.