pygeoapi: publish your first OGC API with test data
Build a pygeoapi 0.24 CSV endpoint with full configuration, OpenAPI and executable checks for collections, features and missing identifiers.
Editorial review: 2026-09-23 · Tested with: pygeoapi 0.24.0 / Python 3.13 — CSV API smoke test
Build a local OGC API Features service with two synthetic stations, a discoverable collection and stable item URLs. You will create the data and complete configuration, generate OpenAPI, inspect responses and distinguish this small exercise from a production service.
The CSV API was smoke-tested with pygeoapi 0.24.0 and Python 3.13. That test covered discovery, the OpenAPI endpoint, conformance, bounded item retrieval and a missing identifier. It does not establish production capacity, external authentication or PostgreSQL behavior. The larger downloadable reference lab uses a separate assets collection; do not mix its names with the stations exercise below.
What pygeoapi does in this architecture
pygeoapi connects a published API resource to a data provider. The provider reads the source; the API exposes its collection, item links and supported query behavior. You do not need to convert every dataset into a proprietary hosted layer before making it accessible to software. You still need data quality, permissions, maintenance and a stable public contract.
For this exercise the source is CSV, the geometry is a point built from longitude/latitude columns, and the output is GeoJSON. CSV is a useful first step because there is no database network or privilege problem to diagnose. Once the contract works, move to the PostGIS provider guide.
1. Prepare an empty directory and synthetic data
Use an empty working directory. Save this file as stations.csv:
id,name,longitude,latitude
s1,Station A,-75.58,6.24
s2,Station B,-75.57,6.25
These stations do not represent real infrastructure or observations. The license below applies to these invented records only; changing a configuration field does not relicense somebody else's dataset. A real publication needs the original owner, license, update date and access conditions.
Identifiers should remain stable when the file is reordered or republished. Do not use row position as an external identifier. Keep coordinates as longitude then latitude: valid JSON with reversed coordinates can still locate a station in the wrong part of the world.
2. Save the complete configuration
Save as config.yml in the same directory. server.url is the address advertised to consumers; it must match how they access this local service. admin: false keeps this exercise focused on publication. max_items limits one page, not the total amount a determined client could collect over multiple requests.
server:
bind: {host: 127.0.0.1, port: 5000}
url: http://localhost:5000
mimetype: application/json; charset=UTF-8
encoding: utf-8
languages: [en-US]
pretty_print: true
admin: false
limits: {default_items: 10, max_items: 100}
logging: {level: ERROR}
metadata:
identification:
title: Synthetic stations lab
description: Two fictional stations for local learning
keywords: [synthetic, stations]
keywords_type: theme
terms_of_service: https://creativecommons.org/publicdomain/zero/1.0/
url: https://example.org
license: {name: CC0, url: https://creativecommons.org/publicdomain/zero/1.0/}
provider: {name: Local lab, url: https://example.org}
contact: {name: Lab operator, email: lab@example.org}
resources:
stations:
type: collection
title: Synthetic stations
description: Fictional points, not operational observations
keywords: [synthetic]
links: []
extents:
spatial:
bbox: [-75.7, 6.1, -75.5, 6.3]
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
providers:
- type: feature
name: CSV
data: stations.csv
id_field: id
geometry: {x_field: longitude, y_field: latitude}
The metadata identifies the service and source. The collection extent describes the published area and is not a row-level access filter. The provider specifies the file, ID field and coordinate columns. A misspelled column can break geometry construction even while the YAML itself parses correctly.
The pinned 0.24.0 configuration reference describes these settings. Keep version-specific documentation with the environment; a latest documentation page may describe a development release.
3. Install and generate the API description
Run from the directory containing both files:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install pygeoapi==0.24.0
export PYGEOAPI_CONFIG="$PWD/config.yml"
export PYGEOAPI_OPENAPI="$PWD/openapi.yml"
pygeoapi openapi generate "$PYGEOAPI_CONFIG" --output-file "$PYGEOAPI_OPENAPI"
pygeoapi serve
The server stays in the foreground; stop with Ctrl+C. This is a local development server. The environment variables point to absolute paths, while this small CSV path is relative to the working directory. If you start from another directory, use a suitable absolute data path or a controlled service working directory.
Regenerate OpenAPI when the publication configuration changes. A server returning a new collection while its API description still advertises the previous configuration confuses consumers and automated clients. The running guide explains the generation and server entry points.
4. Read the API in the order a consumer discovers it
Open the local service in a browser, then inspect these JSON resources. The exact values and links matter more than whether the page looks attractive.
| Resource | What to verify |
|---|---|
/collections?f=json | stations appears with a useful description |
/collections/stations?f=json | Extent and item links describe this dataset |
/collections/stations/items?f=json&limit=1 | One feature with a stable ID |
/collections/stations/items/s1?f=json | The expected station and coordinates |
/conformance?f=json | Advertised conformance classes |
/openapi?f=json | Machine-readable API description |
A conformance declaration describes claimed protocol support; it is not proof that every data provider implements every optional query. Consult the provider matrix for the installed version and test required behaviors against the chosen source.
5. Automate the essential checks
Save the following as check_api.py and run python check_api.py in another terminal while the server runs:
import json
from urllib.error import HTTPError
from urllib.request import urlopen
base = 'http://localhost:5000'
def get_json(path):
with urlopen(base + path, timeout=10) as response:
assert response.status == 200
return json.load(response)
collections = get_json('/collections?f=json')
assert 'stations' in [item['id'] for item in collections['collections']]
page = get_json('/collections/stations/items?f=json&limit=1')
assert page['type'] == 'FeatureCollection'
assert len(page['features']) == 1
assert page['features'][0]['id'] == 's1'
assert page['features'][0]['geometry']['coordinates'] == [-75.58, 6.24]
item = get_json('/collections/stations/items/s1?f=json')
assert item['id'] == 's1'
try:
get_json('/collections/stations/items/missing?f=json')
except HTTPError as error:
assert error.code == 404
else:
raise AssertionError('Missing identifier must not return another station')
print('Collection, item, coordinates and missing identifier checks passed')
The checks verify IDs and coordinates, not just response status. Add a request with an excessive limit and inspect the configured behavior. With only two records, returning two cannot prove that a maximum of 100 is enforced; use the larger downloadable fixture to test a real upper bound. This distinction prevents a weak test from becoming a false claim.
6. Understand pagination and data changes
Read pagination links from the response when provided instead of inventing offset URLs. Keep a record of retrieved IDs and reject unexpected duplicates when assembling an export. If the source changes during pagination, decide whether your use case requires a stable snapshot, an update timestamp or a dedicated export.
A page-size limit protects response size but does not by itself bound expensive filtering, database time or total downloads. A public API may need rate limits, connection limits and a bulk-data alternative. Authentication and authorization also remain separate from metadata and licensing.
7. Diagnose failures systematically
| Symptom | Check | Expected repair |
|---|---|---|
| Missing CSV | Process working directory and file path | Start in the intended directory or set absolute path |
| Collection exists but items fail | Headers and geometry fields | Match id, longitude, latitude exactly |
| Wrong location | Coordinate order and units | Preserve longitude/latitude degrees |
| Changed collection absent from OpenAPI | Generated file revision | Regenerate and restart |
| HTML returned to software | Negotiated representation | Use f=json or an appropriate Accept header |
| Unknown ID returns a valid feature | Identifier/provider behavior | Fix before consumers rely on item URLs |
| Large limit seems allowed | Dataset smaller than limit | Use a larger fixture and inspect count |
8. Move toward a service others can depend on
Before public deployment, replace synthetic metadata, define update ownership, decide public attributes, add stable HTTPS URLs and run the tests through the actual proxy. Test restart and file replacement with the process account. Write down how to restore the previous publication if an update is malformed.
Continue with PostGIS filtering and pagination and production deployment. If the consumer needs server-rendered cartography or legacy WMS/WFS, compare GeoServer, pygeoapi and QGIS Server before selecting the service solely because its first endpoint was easy to create.