Geospatial APIs: PostGIS + FastAPI + spatial REST
Most geospatial work ends with someone wanting a JSON API. The notebook that detected the plume, the model that classified the reef, the pipeline that tracked the ship — none of it matters until other people can query it. This week you build that endpoint properly: FastAPI in front, PostGIS behind, auth and rate limits at the door, OGC API patterns on the wire.
If you built a tool that detects red-tide blooms along Hawaiian coastlines, how would you let other people use your tool?
Through an API. This week you'll learn the patterns — same patterns the State Climate Data Portal uses, same patterns NOAA uses. By the end, your work can serve other people's work.
Learning objectives
- Build a FastAPI app that wraps a PostGIS database with the four canonical spatial query patterns (by id, bbox, point+radius, polygon)
- Add API-key auth and per-key rate limiting; set CORS headers and cache-control correctly
- Return OGC API — Features compliant GeoJSON FeatureCollections with cursor-based pagination
- Ship with auto-generated OpenAPI 3.0 docs and a versioned URL scheme
Primer
The endpoint where space GIS meets the rest of the world is almost always a REST API. Someone downstream — another team, a paying customer, a frontend app — wants geospatial data over HTTP, with predictable URLs, ergonomic parameters, and JSON responses. This week you build that endpoint properly.
The shape of a spatial REST endpoint
The four query patterns that show up everywhere:
- By ID — single feature by its identifier.
- By bounding box — features inside a lon_min, lat_min, lon_max, lat_max box. Useful for map tiles.
- By point and radius — features within radius_km of a lat, lon. Useful for near-me queries.
- By polygon — features inside an arbitrary GeoJSON polygon. POST it as the request body.
Common parameters across all of them: time range, pagination, attribute filtering, sorting.
FastAPI plus PostGIS
The minimal stack:
from fastapi import FastAPI, Query
from sqlalchemy import create_engine, text
engine = create_engine("postgresql://...")
app = FastAPI(title="LaunchDetect API")
@app.get("/detections")
def detections(bbox: str | None = None, limit: int = 50):
if bbox:
lon_min, lat_min, lon_max, lat_max = map(float, bbox.split(","))
sql = text("SELECT id, ST_AsGeoJSON(position) AS geom FROM detections "
"WHERE position && ST_MakeEnvelope(:l1,:l2,:l3,:l4,4326) "
"ORDER BY detected_at DESC LIMIT :lim")
with engine.begin() as conn:
rows = conn.execute(sql, dict(l1=lon_min,l2=lat_min,l3=lon_max,l4=lat_max,lim=limit)).all()
return rows
OpenAPI for free
FastAPI auto-generates a full OpenAPI 3.0 specification from your function signatures and Pydantic models. Visit /docs for interactive Swagger UI, /redoc for ReDoc. Other teams can generate client SDKs in any language from the OpenAPI spec.
Pagination
Spatial endpoints often return many features. Two approaches:
- Offset pagination — easy to implement but degrades with large offsets and breaks if features are inserted or deleted between pages.
- Cursor pagination — a cursor parameter encodes the last item seen. Stable under writes, scales to any depth. Use the Link header per RFC 5988.
For high-traffic endpoints, cursor is the right default.
Caching
Spatial responses cache well. Set Cache-Control: public, max-age=60 on bbox queries. Use a CDN in front. For Lambda-fronted APIs (the architecture you built in Week 27), CDN caching can drop your origin load by 95 percent or more — and your cost with it. Don't cache user-scoped or rate-limited responses; vary by Authorization header if needed.
Auth, rate limits, and abuse
Public geospatial APIs get hammered the moment they're discovered. Three layers of defense, in order:
- API keys — minimum bar. Issue per-user keys; clients send them in an
X-Api-Keyheader. Pair with usage metering so you know who's calling what. - Rate limiting — fixed-window or token-bucket. Public unauthenticated: 60 req/min. Authenticated: 600 req/min. Paid tier: higher. Return
429 Too Many RequestswithRetry-AfterandX-RateLimit-*headers so well-behaved clients back off. - OAuth2 / scopes — for partner integrations and anything writeable. Use bearer tokens with scope claims (
read:detections,read:tracks); validate at the edge.
CORS and response design
If a browser frontend will call your API directly, you need CORS. Set Access-Control-Allow-Origin precisely (a whitelist, not *, if you ever return auth-gated data). For public read-only endpoints, * is fine.
Design the response shape with the consumer in mind. Always return a GeoJSON FeatureCollection (not a raw array of features) so clients can extend with metadata: a top-level numberMatched + numberReturned field, a links array for pagination cursors, and per-feature properties with typed values (ISO 8601 timestamps, not Unix ints; integer IDs, not strings).
OGC API — Features (the standard)
Don't reinvent the URL scheme. The OGC API — Features standard (the successor to WFS) formalizes the four patterns above into a stable contract: GET /collections, GET /collections/{id}, GET /collections/{id}/items?bbox=...&datetime=..., GET /collections/{id}/items/{featureId}. Stable URLs, predictable parameters, a conformance document so clients know what's supported. Adopting it for free gets you compatibility with QGIS, pygeoapi, and every other OGC-aware tool. STAC (SpatioTemporal Asset Catalog) is built on top of OGC API — Features for satellite imagery specifically.
Versioning
Public APIs are contracts. Use /v1/ in the path; reserve /v2/ for breaking changes. Add a Deprecation header on endpoints scheduled for removal so clients see it coming. Support the previous version for at least six months after deprecation.
One more responsibility
An API broadcasts your data to whoever finds it. Before you flip the switch on a public endpoint, re-read Week 28. Sub-meter coordinates, real-time launch detections, vessel tracks — these can carry export-control implications (ITAR) or privacy harm even when individual records seem innocuous. The right answer is sometimes "this endpoint authenticates" or "this endpoint exists only inside the partner VPC."
The lab
You'll build a FastAPI app that wraps a PostGIS detections table, exposes the four endpoint patterns above with API-key auth and per-key rate limits, returns OGC API — Features compliant GeoJSON, and ships with auto-generated OpenAPI docs at /docs. This is the same architecture in production at launchdetect.com/space-data-api/.
Connecting to Hawaiʻi: Hawaiian data APIs and reciprocity
The Pacific Islands Ocean Observing System (PacIOOS) at the University of Hawaiʻi publishes real-time Pacific oceanographic data via APIs — wave height, sea surface temperature, currents, ocean acidification — all free, all open, all available to any developer. The Department of Land and Natural Resources has been steadily moving its datasets into accessible APIs. When you build an API for your own geospatial work, you're joining this network. Knowledge flows both ways: you use their data, your tools can give back.
Hands-on lab: Spatial REST API for launch detections
Build a FastAPI app with bbox / radius / id endpoints over a PostGIS detections table. Returns GeoJSON.
Quiz — click an answer to check it
No grade, no shame. Tap any option; you'll see if it's right plus the answer if not. The point is to notice what you already know and what's still settling.
- OpenAPI spec and interactive Swagger UI at /docs
- Database migration
- Frontend
- Hosting
- ST_MakeEnvelope to build the bbox geometry, then ST_Intersects or && operator
- Just SQL LIKE
- JSON parsing
- Random sampling
- A FeatureCollection containing an array of Feature objects
- Just a list of coordinates
- A binary blob
- XML
- Cursor-based with cursor parameter or HTTP Link header
- Offset only
- No pagination
- Random
- GeoJSON is human-readable and web-friendly; EWKB is more compact
- EWKB is always better
- Same thing
- Neither matters
Reflection
Take five minutes with this. Write your answer somewhere. Carry it into next week.