GeoQuery / docs /backend /API_ENDPOINTS.md
GerardCB's picture
Deploy to Spaces (Final Clean)
4851501

API Endpoints Reference

Complete reference for GeoQuery's FastAPI endpoints.


Base URL

Development: http://localhost:8000
Production: Configure via environment


Endpoints

1. Chat Query (SSE Streaming)

Main endpoint for natural language queries with streaming responses.

POST /api/chat
Content-Type: application/json

Request Body:

{
  "message": "Show me hospitals in Panama City",
  "history": [
    {"role": "user", "content": "previous query"},
    {"role": "assistant", "content": "previous response"}
  ]
}

Response: Server-Sent Events (SSE) stream

Event Types:

  1. status - Processing status updates

    {"status": "🔍 Identifying relevant tables..."}
    
  2. intent - Detected query intent

    {"intent": "MAP_REQUEST"}
    
  3. chunk - Streaming content (text or thought)

    {"type": "thought", "content": "Planning spatial query..."}
    {"type": "text", "content": "I found 45 hospitals..."}
    
  4. result - Final result with data

    {
      "response": "Full explanation text",
      "sql_query": "SELECT name, geom FROM ...",
      "geojson": {...},
      "chart_data": {...},
      "raw_data": [...],
      "data_citations": [...]
    }
    

curl Example:

curl -X POST http://localhost:8000/api/chat \  
  -H "Content-Type: application/json" \
  -d '{"message": "Show me provinces", "history": []}' \
  --no-buffer

JavaScript Example:

const response = await fetch('http://localhost:8000/api/chat', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    message: "Show me hospitals",
    history: []
  })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  
  const chunk = decoder.decode(value);
  const lines = chunk.split('\n');
  
  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = JSON.parse(line.slice(6));
      console.log(data);
    }
  }
}

2. Get Data Catalog

Returns metadata for all available datasets.

GET /api/catalog

Response:

{
  "panama_healthsites_geojson": {
    "description": "Healthcare facilities including hospitals...",
    "categories": ["health", "infrastructure"],
    "tags": ["hospitals", "clinics"],
    "row_count": 986,
    "has_geometry": true
  },
  "pan_admin1": {
    "description": "Panama provinces...",
    "categories": ["administrative"],
    "row_count": 10,
    "has_geometry": true
  }
}

curl Example:

curl http://localhost:8000/api/catalog

3. Get Database Schema

Returns current database schema with loaded tables.

GET /api/schema

Response:

{
  "tables": [
    {
      "name": "panama_healthsites_geojson",
      "columns": ["osm_id", "name", "amenity", "geom"],
      "row_count": 986,
      "geometry_type": "Point"
    },
    {
      "name": "pan_admin1",
      "columns": ["adm1_name", "adm1_pcode", "area_sqkm", "geom"],
      "row_count": 10,
      "geometry_type": "Polygon"
    }
  ],
  "loaded_count": 2,
  "total_datasets": 108
}

curl Example:

curl http://localhost:8000/api/schema

Response Formats

GeoJSON Structure

Map data returned in standard GeoJSON format with custom properties:

{
  "type": "FeatureCollection",
  "properties": {
    "layer_id": "abc123",
    "layer_name": "Hospitals in David",
    "style": {
      "color": "#E63946",
      "fillColor": "#E63946",
      "opacity": 0.8,
      "fillOpacity": 0.4
    },
    "pointMarker": {
      "icon": "🏥",
      "style": "icon",
      "color": "#E63946",
      "size": 32
    },
    "choropleth": {
      "enabled": false
    }
  },
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [-79.5, 8.98]
      },
      "properties": {
        "name": "Hospital Santo Tomás",
        "amenity": "hospital"
      }
    }
  ]
}

Properties Explanation:

  • layer_id: Unique identifier
  • layer_name: Display name
  • style: Default polygon/line styling
  • pointMarker: Point rendering configuration
    • style: "icon" → Use emoji icon
    • style: "circle" → Use simple circle
  • choropleth: Choropleth configuration (if enabled)

Chart Data Structure

{
  "type": "bar",
  "title": "Districts by Province",
  "data": {
    "labels": ["Panamá", "Chiriquí", "Veraguas"],
    "datasets": [{
      "label": "District Count",
      "data": [8, 13, 12],
      "backgroundColor": "#6366f1"
    }]
  }
}

Chart Types:

  • bar - Bar chart
  • pie - Pie chart
  • line - Line chart

Data Citations

[
  "Administrative boundary data from HDX/INEC, 2021",
  "Healthcare facilities from OpenStreetMap via Healthsites.io"
]

Error Responses

Standard Error Format

{
  "detail": "Error message",
  "error_type": "DataNotFound",
  "timestamp": "2026-01-10T12:00:00Z"
}

HTTP Status Codes:

  • 400 Bad Request - Invalid query format
  • 404 Not Found - Endpoint not found
  • 500 Internal Server Error - Server error
  • 503 Service Unavailable - LLM API unavailable

Data Unavailable Response

When requested data doesn't exist:

{
  "response": "I couldn't find data for crime statistics in the current database...",
  "sql_query": "-- ERROR: DATA_UNAVAILABLE\\n-- Requested: crime statistics\\n-- Available: admin boundaries, hospitals",
  "geojson": null,
  "data_citations": []
}

CORS Configuration

Current CORS settings (in backend/main.py):

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

For Production: Update allow_origins to your frontend domain.


Rate Limiting

Current: No rate limiting implemented

Recommended (future):

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/chat")
@limiter.limit("10/minute")
async def chat(request: Request):
    ...

Authentication

Current: No authentication required

For Production: Add API key or JWT authentication:

from fastapi import Security, HTTPException
from fastapi.security import APIKeyHeader

API_KEY = os.getenv("API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key")

def verify_api_key(api_key: str = Security(api_key_header)):
    if api_key != API_KEY:
        raise HTTPException(status_code=403, detail="Invalid API key")
    return api_key

WebSocket Support

Current: Not implemented (using SSE instead)

Rationale: SSE sufficient for one-way server → client streaming. WebSockets add complexity without benefit for this use case.


API Versioning

Current: No versioning

Future Consideration:

/api/v1/chat
/api/v2/chat

OpenAPI Documentation

FastAPI auto-generates interactive API docs:


Client Libraries

Python Client Example

import requests
import json

def query_geoquery(message, history=[]):
    response = requests.post(
        "http://localhost:8000/api/chat",
        json={"message": message, "history": history},
        stream=True
    )
    
    for line in response.iter_lines():
        if line.startswith(b'data: '):
            data = json.loads(line[6:])
            if data.get("event") == "chunk":
                print(data["data"]["content"], end="")
            elif data.get("event") == "result":
                return data["data"]

result = query_geoquery("Show me hospitals")
print(result["sql_query"])

JavaScript/TypeScript Client

See frontend/src/lib/api.ts for full implementation.


Performance Considerations

Response Times

Typical query pipeline:

  1. Intent detection: ~500ms
  2. Semantic search: <10ms
  3. SQL generation: ~1s
  4. Query execution: 100ms - 5s (depends on data size)
  5. Explanation: ~1s (streaming)

Total: 2-8 seconds for most queries

Optimization Tips

  1. Pre-load Common Tables: Load frequently used datasets at startup
  2. Query Limits: Add LIMIT clauses for large result sets
  3. Spatial Indexes: DuckDB auto-indexes geometry columns
  4. Caching: Consider Redis for repeated queries

Next Steps