A centralized platform for managing, indexing, and providing STAC (SpatioTemporal Asset Catalog) Collection metadata from distributed catalogs and APIs.
- Getting Started
- API Endpoints
- Query Parameters
- CQL2 Filtering
- Response Format
- Error Handling
- Rate Limiting and Request Size Limits
- API Documentation
- Technical Architecture
- Testing
- STAC Conformance
- Project Structure
- License
- Node.js version 22.0.0 or higher
- PostgreSQL with PostGIS extension (for spatial queries)
- npm or yarn package manager
- Clone the repository and navigate to the API directory:
cd api- Install dependencies:
npm install- Create a local environment file from the example:
cp .env.example .env- Edit
.envand configure your database connection (see Configuration).
The API is configured using environment variables. Copy .env.example to .env and adjust the following settings:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the API server listens on |
NODE_ENV |
development |
Environment mode (development, production, test) |
You can configure the database using either a connection string or individual variables:
Option 1: Connection String
DATABASE_URL=postgresql://stac_api:password@localhost:5432/stac_dbOption 2: Individual Variables
DB_HOST=localhost
DB_PORT=5432
DB_NAME=stac_db
DB_USER=stac_api
DB_PASSWORD=your_password
DB_SSL=false| Variable | Default | Description |
|---|---|---|
DB_POOL_MAX |
20 |
Maximum connections in pool |
DB_POOL_MIN |
2 |
Minimum connections in pool |
DB_IDLE_TIMEOUT |
30000 |
Idle connection timeout (ms) |
DB_CONNECTION_TIMEOUT |
10000 |
Connection timeout (ms) |
| Variable | Default | Description |
|---|---|---|
CORS_ORIGIN |
* |
Allowed CORS origins |
LOG_LEVEL |
debug |
Logging verbosity |
Development mode (with auto-reload on change):
npm run devProduction mode:
npm startThe API will be available at http://localhost:3000.
Build and run the API using Docker:
# Build the image
docker build -t stac-atlas-api .
# Run with docker-compose
docker-compose upThe Dockerfile uses Node.js 22 Alpine and exposes port 3000.
All endpoints return JSON responses with Content-Type: application/json.
GET /
Returns the STAC API landing page with links to all available resources.
Example Request:
curl http://localhost:3000/Example Response:
{
"type": "Catalog",
"id": "stac-atlas",
"title": "STAC Atlas",
"description": "A centralized platform for managing, indexing, and providing STAC Collection metadata from distributed catalogs and APIs.",
"stac_version": "1.0.0",
"conformsTo": ["https://api.stacspec.org/v1.0.0/core", "..."],
"links": [
{"rel": "self", "href": "http://localhost:3000", "type": "application/json"},
{"rel": "conformance", "href": "http://localhost:3000/conformance", "type": "application/json"},
{"rel": "data", "href": "http://localhost:3000/collections", "type": "application/json"},
{"rel": "health", "href": "http://localhost:3000/health", "type": "application/json"},
{"rel": "queryables", "href": "http://localhost:3000/collection-queryables", "type": "application/schema+json"},
{"rel": "service-doc", "href": "http://localhost:3000/api-docs", "type": "text/html"},
{"rel": "service-desc", "href": "http://localhost:3000/openapi.yaml", "type": "application/vnd.oai.openapi+json;version=3.0"}
]
}GET /conformance
Returns the list of conformance classes implemented by the API.
Example Request:
curl http://localhost:3000/conformanceExample Response:
{
"conformsTo": [
"https://api.stacspec.org/v1.0.0/core",
"https://api.stacspec.org/v1.0.0/collections",
"https://api.stacspec.org/v1.0.0/collection-search",
"http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
"http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators",
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-json",
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
"http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-functions",
"http://www.opengis.net/spec/cql2/1.0/conf/spatial-functions",
"http://www.opengis.net/spec/cql2/1.0/conf/temporal-functions"
]
}GET /collections
Returns a paginated list of STAC Collections with optional filtering.
See Query Parameters and CQL2 Filtering for filtering options.
Example Request:
curl "http://localhost:3000/collections?limit=10&q=sentinel"Example Response:
{
"collections": [
{
"type": "Collection",
"stac_version": "1.0.0",
"id": "sentinel-2-l2a",
"stac_id": "sentinel-2-l2a",
"source_id": "sentinel-2-l2a",
"source_url": "https://example.com/stac/collections/sentinel-2-l2a",
"title": "Sentinel-2 Level-2A",
"description": "Sentinel-2 atmospherically corrected surface reflectance",
"license": "CC-BY-4.0",
"extent": {
"spatial": {"bbox": [[-180, -90, 180, 90]]},
"temporal": {"interval": [["2015-06-27T00:00:00Z", null]]}
},
"links": [
{"rel": "self", "href": "http://localhost:3000/collections/sentinel-2-l2a"},
{"rel": "root", "href": "http://localhost:3000"},
{"rel": "parent", "href": "http://localhost:3000"},
{"rel": "items", "href": "https://example.com/stac/collections/sentinel-2-l2a/items", "title": "Source Item Reference"}
]
}
],
"links": [
{"rel": "self", "href": "http://localhost:3000/collections?limit=10&q=sentinel"},
{"rel": "root", "href": "http://localhost:3000"},
{"rel": "next", "href": "http://localhost:3000/collections?limit=10&token=10&q=sentinel"}
],
"context": {
"returned": 10,
"limit": 10,
"matched": 42
}
}GET /collections/{collectionId}
Returns a single STAC Collection by its identifier.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
collectionId |
string | Collection identifier |
Example Request:
curl http://localhost:3000/collections/sentinel-2-l2aResponse: A single STAC Collection object (same structure as in the collections list).
Error Response (404):
{
"type": "https://stacspec.org/errors/NotFound",
"title": "Not Found",
"status": 404,
"code": "NotFound",
"description": "Collection with id 'unknown-collection' not found",
"instance": "/collections/unknown-collection",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}GET /collection-queryables
Returns a JSON Schema describing properties that can be used in CQL2 filter expressions.
Example Request:
curl http://localhost:3000/collection-queryablesExample Response (abbreviated):
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "http://localhost:3000/collection-queryables",
"type": "object",
"title": "STAC Atlas Collections Queryables",
"properties": {
"id": {
"title": "Collection ID",
"type": ["string", "integer"],
"x-ogc-operators": ["=", "<>", "<", "<=", ">", ">=", "between", "in", "isNull", "like"]
},
"title": {
"title": "Title",
"type": "string",
"x-ogc-operators": ["=", "<>", "<", "<=", ">", ">=", "between", "in", "isNull", "like"]
},
"license": {
"title": "License",
"type": "string",
"x-ogc-operators": ["=", "<>", "<", "<=", ">", ">=", "between", "in", "isNull", "like"]
},
"spatial_extent": {
"title": "Spatial Extent",
"type": "object",
"x-ogc-operators": ["s_intersects", "s_within", "s_contains", "isNull"]
}
},
"links": [...]
}GET /health
Returns health status and readiness information for monitoring and Kubernetes probes.
Example Request:
curl http://localhost:3000/healthExample Response (healthy):
{
"type": "Health",
"id": "stac-atlas-health",
"title": "STAC Atlas API Health Check",
"description": "Health status and readiness information for the STAC Atlas API",
"status": "ok",
"ready": true,
"uptimeSec": 3600,
"timestamp": "2026-01-31T12:00:00.000Z",
"checks": {
"alive": {"status": "ok"},
"db": {"status": "ok", "latencyMs": 5}
},
"links": [
{"rel": "self", "href": "http://localhost:3000/health"},
{"rel": "root", "href": "http://localhost:3000"},
{"rel": "parent", "href": "http://localhost:3000"}
]
}Response when database is unavailable (503):
{
"type": "Health",
"status": "degraded",
"ready": false,
"checks": {
"alive": {"status": "ok"},
"db": {"status": "error", "latencyMs": 150, "code": "ECONNREFUSED", "message": "Database connectivity check failed"}
}
}| Status Code | Meaning |
|---|---|
| 200 | Service is healthy and ready |
| 503 | Service is alive but degraded (database unavailable) |
All query parameters for GET /collections are optional and can be combined.
Search across collection title, description, and keywords using PostgreSQL full-text search.
| Constraint | Value |
|---|---|
| Maximum length | 500 characters |
Examples:
# Search for "sentinel"
GET /collections?q=sentinel
# Search for multiple terms (AND logic)
GET /collections?q=landsat%20climateFilter collections by spatial extent intersection.
Format: minLon,minLat,maxLon,maxLat (WGS84 coordinates)
| Constraint | Value |
|---|---|
| Longitude | -180 to 180 |
| Latitude | -90 to 90 |
| Coordinates | Exactly 4 values |
Examples:
# Collections in Germany
GET /collections?bbox=5.9,47.3,15.0,55.1
# Collections in California
GET /collections?bbox=-124.4,32.5,-114.1,42.0Filter collections by temporal extent overlap.
Supported formats:
| Format | Example | Description |
|---|---|---|
| Single | 2020-01-01T00:00:00Z |
Exact timestamp |
| Interval | 2020-01-01/2025-12-31 |
Closed interval |
| Open start | ../2025-12-31 |
Everything before date |
| Open end | 2020-01-01/.. |
Everything after date |
Examples:
# Collections from 2020
GET /collections?datetime=2020-01-01T00:00:00Z/2020-12-31T23:59:59Z
# Collections before 2020
GET /collections?datetime=../2019-12-31
# Collections after 2023
GET /collections?datetime=2023-01-01/..Control the number of results and navigate through pages.
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
limit |
integer | 10 | 1-10000 | Maximum results per page |
token |
integer | 0 | 0+ | Offset (number of results to skip) |
Pagination workflow:
- Initial request:
GET /collections?limit=20 - Check
context.matchedfor total results - Follow
nextlink in response:GET /collections?limit=20&token=20 - Continue until no
nextlink is present
Examples:
# First 20 results
GET /collections?limit=20
# Results 21-40
GET /collections?limit=20&token=20
# Results 41-60
GET /collections?limit=20&token=40Sort results by a specific field.
Format: [+|-]fieldname
| Prefix | Direction |
|---|---|
+ or none |
Ascending (A-Z, oldest first) |
- |
Descending (Z-A, newest first) |
Available fields:
| Field | Description |
|---|---|
title |
Collection title (alphabetical) |
id |
Collection identifier |
license |
License identifier |
created |
Creation timestamp |
updated |
Last update timestamp |
Examples:
# Newest first
GET /collections?sortby=-created
# Alphabetical by title
GET /collections?sortby=+title
# Most recently updated
GET /collections?sortby=-updatedFilter by provider name or license identifier.
| Parameter | Type | Max Length | Description |
|---|---|---|---|
provider |
string | 255 | Filter by provider name (partial match) |
license |
string | 255 | Filter by license identifier |
Examples:
# Collections from USGS
GET /collections?provider=USGS
# Open data collections
GET /collections?license=CC-BY-4.0
# Combine with other parameters
GET /collections?provider=ESA&license=CC-BY-4.0&sortby=-createdFilter collections by their active or API status.
| Parameter | Type | Description |
|---|---|---|
active |
boolean | Filter by collection active status (true/false) |
api |
boolean | Filter by API status (true = from STAC API, false = from static catalog) |
Accepted values: true, false, 1, 0, yes, no
Examples:
# Only active collections
GET /collections?active=true
# Only collections from STAC APIs
GET /collections?api=true
# Active collections from static catalogs
GET /collections?active=true&api=false
# Combine with other filters
GET /collections?active=true&api=true&license=CC-BY-4.0The API supports the Common Query Language 2 (CQL2) standard for advanced filtering. Both CQL2-Text (human-readable) and CQL2-JSON (machine-readable) encodings are supported.
| Parameter | Type | Default | Description |
|---|---|---|---|
filter |
string | - | CQL2 filter expression |
filter-lang |
string | cql2-text |
Language: cql2-text or cql2-json |
Important rules for CQL2-Text:
- String literals must be enclosed in single quotes:
'value' - Property names are written without quotes:
license,title - Operators are case-insensitive:
AND,and,And
Common mistake:
Correct: license = 'CC-BY-4.0'
Wrong: license = CC-BY-4.0 (CC-BY-4.0 is interpreted as a property)
| Operator | Description | Example |
|---|---|---|
= |
Equal | license = 'CC-BY-4.0' |
<> |
Not equal | license <> 'proprietary' |
< |
Less than | field < 100 |
> |
Greater than | field > 50 |
<= |
Less than or equal | field <= 100 |
>= |
Greater than or equal | field >= 1 |
Examples:
GET /collections?filter=license = 'CC-BY-4.0'
GET /collections?filter=field >= 10| Operator | Description |
|---|---|
AND |
Both conditions must be true |
OR |
At least one condition must be true |
NOT |
Negates a condition |
Examples:
# Both conditions
GET /collections?filter=license = 'CC-BY-4.0' AND title LIKE '%Sentinel%'
# Either condition
GET /collections?filter=license = 'MIT' OR license = 'Apache-2.0'
# Negation
GET /collections?filter=NOT license = 'proprietary'| Operator | Description | Example |
|---|---|---|
BETWEEN |
Value within range (inclusive) | id BETWEEN 10 AND 50 |
IN |
Value in list | license IN ('MIT', 'Apache-2.0') |
IS NULL |
Value is null | description IS NULL |
LIKE |
Pattern matching | title LIKE '%Sentinel%' |
Examples:
GET /collections?filter=field BETWEEN 1 AND 100
GET /collections?filter=license IN ('MIT', 'CC0-1.0', 'CC-BY-4.0')
GET /collections?filter=title IS NULLThe LIKE operator supports SQL-style wildcard patterns:
| Wildcard | Description | Example Match |
|---|---|---|
% |
Zero or more characters | '%Sentinel%' matches "Sentinel-2", "Copernicus Sentinel" |
_ |
Exactly one character | 'Sentinel-_' matches "Sentinel-1", "Sentinel-2" |
Examples:
# Contains "Sentinel"
GET /collections?filter=title LIKE '%Sentinel%'
# Starts with "USGS"
GET /collections?filter=title LIKE 'USGS%'
# Ends with "L2A"
GET /collections?filter=title LIKE '%L2A'
# Sentinel followed by single character
GET /collections?filter=title LIKE 'Sentinel-_'CQL2-JSON format:
{
"op": "like",
"args": [{"property": "title"}, "%Sentinel%"]
}Note: Pattern matching is case-sensitive. Use the q parameter for case-insensitive full-text search.
Spatial operators filter collections based on geometry relationships using PostGIS.
| Operator | Description |
|---|---|
S_INTERSECTS |
Geometries share any space |
S_WITHIN |
Collection extent is within geometry |
S_CONTAINS |
Collection extent contains geometry |
CQL2-JSON Example:
# Collections intersecting a bounding box around Muenster
GET /collections?filter-lang=cql2-json&filter={"op":"s_intersects","args":[{"property":"spatial_extent"},{"type":"Polygon","coordinates":[[[7,51],[8,51],[8,52],[7,52],[7,51]]]}]}Temporal operators filter collections based on time relationships.
| Operator | Description |
|---|---|
T_INTERSECTS |
Temporal extents overlap |
T_BEFORE |
Collection is before timestamp |
T_AFTER |
Collection is after timestamp |
Interval formats:
- Closed:
["2020-01-01", "2025-12-31"] - Open start:
["..", "2025-12-31"] - Open end:
["2020-01-01", ".."]
CQL2-JSON Example:
# Collections from 2020-2025
GET /collections?filter-lang=cql2-json&filter={"op":"t_intersects","args":[{"property":"datetime"},{"interval":["2020-01-01","2025-12-31"]}]}{
"collections": [...],
"links": [
{"rel": "self", "href": "..."},
{"rel": "root", "href": "..."},
{"rel": "next", "href": "..."},
{"rel": "prev", "href": "..."}
],
"context": {
"returned": 10,
"limit": 10,
"matched": 156
}
}| Field | Description |
|---|---|
collections |
Array of STAC Collection objects |
links |
Navigation links including pagination |
context.returned |
Number of collections in this response |
context.limit |
Maximum results per page |
context.matched |
Total collections matching the query |
Each collection includes links to both STAC Atlas and the original source:
| Rel | Description |
|---|---|
self |
This collection in STAC Atlas |
root |
STAC Atlas landing page |
parent |
STAC Atlas landing page |
items / item |
Original source item references |
source_* |
Other links from original source catalog |
All errors follow the RFC 7807 Problem Details format.
Example error response:
{
"type": "https://stacspec.org/errors/InvalidParameter",
"title": "Invalid Parameter",
"status": 400,
"code": "InvalidParameter",
"description": "Parameter 'bbox' must contain exactly 4 coordinates",
"instance": "/collections?bbox=1,2,3",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request - Invalid parameters |
| 404 | Not Found - Resource does not exist |
| 413 | Payload Too Large - Request exceeds size limits |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
| 503 | Service Unavailable - Database unavailable |
All endpoints are protected by rate limiting:
| Setting | Value |
|---|---|
| Requests per window | 1000 |
| Window duration | 15 minutes |
| Scope | Per IP address |
When exceeded, the API returns HTTP 429 with headers:
RateLimit-Limit: Maximum requests allowedRateLimit-Remaining: Requests remaining in windowRateLimit-Reset: Time when limit resets
| Limit | Default | Description |
|---|---|---|
| URL length | 1 MB | Maximum URL including query string |
| Header size | 100 KB | Maximum total header size |
| Body size | 10 MB | Maximum request body (for future POST support) |
These limits can be configured via environment variables:
MAX_URL_LENGTHMAX_HEADER_SIZEMAX_BODY_SIZE
Interactive API documentation is available at:
http://localhost:3000/api-docs
The raw OpenAPI 3.0 specification is available at:
http://localhost:3000/openapi.yaml
| Component | Technology |
|---|---|
| Runtime | Node.js 22+ |
| Framework | Express.js 4.x |
| Database | PostgreSQL with PostGIS |
| CQL2 Parser | cql2-wasm (Rust WASM) |
| Documentation | Swagger UI / OpenAPI 3.0 |
| Logging | Winston |
| Testing | Jest + Supertest |
Requests pass through the following middleware in order:
- Request ID - Assigns unique ID for tracing
- HTTP Logger - Logs request/response details
- Rate Limiting - Prevents abuse
- Request Size Limiting - Protects against oversized requests
- Body Parsing - Parses JSON and URL-encoded bodies
- CORS - Handles cross-origin requests
- Route Handlers - Processes API requests
- Error Handler - Returns standardized error responses
The API connects to PostgreSQL with PostGIS for:
- Full-text search using TSVector
- Spatial queries using PostGIS geometry functions
- Temporal range queries
- JSONB storage for complete STAC Collection metadata
Connection pooling is configured for optimal performance with configurable pool sizes and timeouts.
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch# Linting
npm run lint
# Auto-fix linting issues
npm run lint:fix
# Format code
npm run formatThe API can be validated using the official STAC API Validator:
# Install validator (Python 3.11 required)
pip install stac-api-validator
# Validate core conformance
python -m stac_api_validator --root-url http://localhost:3000 --conformance coreThis API implements the following conformance classes:
| Conformance Class | Status |
|---|---|
| STAC API Core 1.0.0 | Implemented |
| STAC Collections | Implemented |
| Collection Search | Implemented |
| CQL2 Basic | Implemented |
| CQL2 Advanced Comparison | Implemented |
| CQL2 Spatial Functions | Implemented |
| CQL2 Temporal Functions | Implemented |
| CQL2-Text Encoding | Implemented |
| CQL2-JSON Encoding | Implemented |
| Sorting | Implemented |
| Free-Text Search | Implemented |
api/
├── bin/
│ └── www # Server entry point
├── config/
│ ├── conformanceURIS.js # STAC conformance URIs
│ └── queryablesSchema.js # CQL2 queryables definition
├── db/
│ ├── db_APIconnection.js # Database connection pool
│ └── buildCollectionSearchQuery.js # SQL query builder
├── docs/
│ ├── openapi.yaml # OpenAPI specification
│ ├── collection-search-parameters.md
│ └── cql2-filtering.md
├── middleware/
│ ├── cors.js # CORS configuration
│ ├── errorHandler.js # Global error handler
│ ├── rateLimit.js # Rate limiting
│ ├── requestId.js # Request ID generation
│ ├── requestSize.js # Size limit enforcement
│ ├── validateCollectionId.js # Collection ID validation
│ └── validateCollectionSearch.js # Query parameter validation
├── routes/
│ ├── index.js # Landing page (/)
│ ├── conformance.js # Conformance (/conformance)
│ ├── collections.js # Collections (/collections)
│ ├── queryables.js # Queryables (/collection-queryables)
│ └── health.js # Health check (/health)
├── utils/
│ ├── cql2.js # CQL2 parser interface
│ ├── cql2ToSql.js # CQL2 to SQL converter
│ ├── errorResponse.js # RFC 7807 error formatting
│ └── logger.js # Winston logger
├── validators/
│ └── collectionSearchParams.js # Parameter validators
├── __tests__/ # Test files
├── app.js # Express application
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
├── package.json
├── .env.example # Environment template
└── README.md
Apache-2.0
STAC Atlas API Team (Robin Gummels, Vincent Kühn, Jonas Klaer) - University of Muenster, Geosoftware II (Winter Semester 2025/2026)