Short answer
A spatial data API serves geospatial features over HTTP so any client — a web map, a notebook, another service — can request exactly the subset it needs (by bounding box, attribute filter, or CRS) instead of downloading a whole shapefile and filtering locally. The modern standard is OGC API - Features, a REST/JSON successor to WFS, typically backed by PostGIS and fronted by a server like pygeoapi, pg_featureserv, or GeoServer. For visualization-scale data you pair it with vector tiles; for imagery and large raster catalogs you use STAC. Build an API when a dataset is consumed repeatedly by multiple clients; ship a file when it is a stable, one-time handoff.
OGC API - Features vs the old WFS
The Web Feature Service (WFS 1.1/2.0) worked but was XML-heavy, used GML by default, and required verbose GetFeature POST requests. OGC API - Features redesigns the same capability around plain REST and OpenAPI:
GET /collections— list available datasets.GET /collections/{collectionId}— metadata for one dataset (extent, CRS, item count).GET /collections/{collectionId}/items— the features, as GeoJSON.GET /collections/{collectionId}/items/{featureId}— a single feature.
Querying is done with ordinary query parameters: ?bbox=minx,miny,maxx,maxy, ?limit=1000, ?datetime=2026-01-01/.., and attribute filters. The spec is modular: Part 1 (Core) is read-only; Part 2 adds CRS handling; Part 3 (Filtering) introduces CQL2 for expressions like ?filter=lithology='basalt' AND elevation>1500. Because every endpoint is described by an OpenAPI document at /api, clients can self-discover the schema.
This matters for GIS teams because GeoJSON-over-REST is consumable by MapLibre, deck.gl, Leaflet, Python (requests + geopandas.read_file), and R without an XML parser or a GIS desktop in the loop.
The layers of a real spatial data product
A folder of files is not a product. A maintained spatial data product separates concerns:
- Source ingestion — raw deliveries (CSV, shapefile, survey exports) landing in a staging area, with provenance recorded.
- Normalized storage — a PostGIS database where geometry is validated, reprojected to a known CRS, and indexed. This is the source of truth.
- Analysis / derived layers — materialized views or tables computed from the source.
- Publishing layer — OGC API - Features for vector queries, vector tiles for rendering, STAC for imagery, plain downloads for bulk.
- Metadata and versioning — collection descriptions, license, lineage, and an
updatedtimestamp, so a consumer knows what they are pulling.
The payoff is repeatability: a new delivery flows through ingestion, the database updates, and every downstream client sees the new data without anyone re-exporting a shapefile by hand.
Worked example: PostGIS table to live API
Suppose you have a geology.outcrops table in PostGIS and want it queryable over HTTP.
-
Prepare the table. Ensure a single, declared CRS and a spatial index:
ALTER TABLE geology.outcrops ALTER COLUMN geom TYPE geometry(Point, 4326) USING ST_Transform(ST_SetSRID(geom, 4326), 4326); CREATE INDEX idx_outcrops_geom ON geology.outcrops USING GIST (geom);Storing geometry in EPSG:4326 makes GeoJSON output standards-compliant; OGC API - Features defaults its
bboxand output CRS to CRS84 (lon/lat). -
Expose it. With pg_featureserv (a lightweight Go server from the PostGIS authors), point it at the database and it auto-publishes every table and view as a collection:
DATABASE_URL="postgres://user:pw@host/db" pg_featureservThe outcrops table is now at
/collections/geology.outcrops/items. -
Control what is exposed. Rather than publish raw tables, publish views that select only the public columns and rows. pg_featureserv also supports functions (
/functions/...) for parameterized queries — for example a stored function returning outcrops within a buffer of a point. -
Query it. A web client requests only the current map extent:
GET /collections/geology.outcrops/items?bbox=2.1,48.7,2.5,49.0&limit=2000
For a heavier feature set, pygeoapi (Python) adds CQL2 filtering and a richer provider model; GeoServer adds WMS/WMTS and styling if you also need rendered images.
Pagination and payload control
The single most common API failure is returning too many features at once. Defend against it:
- Set a default and maximum
limit(e.g. default 1000, hard cap 10000). Servers exposenumberReturnedandnumberMatchedplusnext/prevlinks — clients should follow them rather than asking for everything. - For render-scale datasets (millions of features), do not page through Features at all — serve vector tiles (Mapbox Vector Tiles /
ST_AsMVTin PostGIS, or a tiles endpoint). Features API is for queries and downloads; tiles are for drawing. - Use bbox filtering server-side so the database, with its GiST index, does the spatial cull — never ship the whole layer and filter in the browser.
- Enable gzip/brotli compression; GeoJSON compresses well, often 80%+.
-- Vector tile generation in PostGIS
SELECT ST_AsMVT(t, 'outcrops') FROM (
SELECT id, lithology,
ST_AsMVTGeom(geom, ST_TileEnvelope(z, x, y)) AS geom
FROM geology.outcrops
WHERE geom && ST_TileEnvelope(z, x, y)
) AS t;
When NOT to build an API
An API is infrastructure you must run, monitor, and secure. Skip it when:
- The dataset is stable and handed off once — ship a GeoPackage (one file, multiple layers, SQLite-based) or a cloud-optimized format (COG for rasters, GeoParquet/FlatGeobuf for vectors) the consumer reads directly from object storage.
- The consumer needs the whole dataset anyway — paging it through an API is slower than a single download.
- There is no recurring update — the maintenance cost of a service buys nothing.
Cloud-optimized files (COG, GeoParquet, PMTiles) increasingly blur this line: a static file on S3 with HTTP range requests gives much of the "query a subset" benefit without a running server.
Common pitfalls and why they happen
- No declared CRS on the source. If PostGIS geometry has SRID 0, GeoJSON output is ambiguous and clients misplace it. Set the SRID explicitly with
ST_SetSRID/ST_Transform. - Publishing raw tables. Exposing the storage schema directly leaks internal columns and couples your API to your database layout. Publish views or functions instead.
- No spatial index. Without a GiST index, every
bboxquery scans the table; latency explodes as the table grows. - Ignoring license and lineage. A dataset served without provenance and redistribution terms is a liability the moment someone reuses it. Put license and source in the collection metadata.
QA and validation
- Hit
/conformanceand confirm the server declares the OGC API classes you depend on. - Validate that returned GeoJSON parses (
geopandas.read_file(url)), geometries are valid (ST_IsValid), and coordinates are in the expected lon/lat order. - Load-test a realistic
bboxquery and confirm the index is used (EXPLAIN ANALYZE). - Verify pagination links round-trip and
numberMatchedmatches a directCOUNT.
Bathyl perspective
We design geological datasets as products, not exports: a versioned PostGIS source of truth, an OGC API - Features endpoint for queries, vector tiles for rendering, and metadata that travels with the data. The test is whether the next update flows through automatically and whether a consumer can see the lineage, CRS, and license without asking. An API is worth running only when reuse is real — otherwise a well-described file is the more honest deliverable.
Related reading
- OGC API Features for Spatial Data
- Designing a Geological Data API
- From PDF Report to Spatial System
- Spatial data products