| # 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. | |
| ```http | |
| POST /api/chat | |
| Content-Type: application/json | |
| ``` | |
| **Request Body**: | |
| ```json | |
| { | |
| "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 | |
| ```json | |
| {"status": "🔍 Identifying relevant tables..."} | |
| ``` | |
| 2. **`intent`** - Detected query intent | |
| ```json | |
| {"intent": "MAP_REQUEST"} | |
| ``` | |
| 3. **`chunk`** - Streaming content (text or thought) | |
| ```json | |
| {"type": "thought", "content": "Planning spatial query..."} | |
| {"type": "text", "content": "I found 45 hospitals..."} | |
| ``` | |
| 4. **`result`** - Final result with data | |
| ```json | |
| { | |
| "response": "Full explanation text", | |
| "sql_query": "SELECT name, geom FROM ...", | |
| "geojson": {...}, | |
| "chart_data": {...}, | |
| "raw_data": [...], | |
| "data_citations": [...] | |
| } | |
| ``` | |
| **curl Example**: | |
| ```bash | |
| curl -X POST http://localhost:8000/api/chat \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"message": "Show me provinces", "history": []}' \ | |
| --no-buffer | |
| ``` | |
| **JavaScript Example**: | |
| ```javascript | |
| 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. | |
| ```http | |
| GET /api/catalog | |
| ``` | |
| **Response**: | |
| ```json | |
| { | |
| "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**: | |
| ```bash | |
| curl http://localhost:8000/api/catalog | |
| ``` | |
| --- | |
| ### 3. Get Database Schema | |
| Returns current database schema with loaded tables. | |
| ```http | |
| GET /api/schema | |
| ``` | |
| **Response**: | |
| ```json | |
| { | |
| "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**: | |
| ```bash | |
| curl http://localhost:8000/api/schema | |
| ``` | |
| --- | |
| ## Response Formats | |
| ### GeoJSON Structure | |
| Map data returned in standard GeoJSON format with custom properties: | |
| ```json | |
| { | |
| "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 | |
| ```json | |
| { | |
| "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 | |
| ```json | |
| [ | |
| "Administrative boundary data from HDX/INEC, 2021", | |
| "Healthcare facilities from OpenStreetMap via Healthsites.io" | |
| ] | |
| ``` | |
| --- | |
| ## Error Responses | |
| ### Standard Error Format | |
| ```json | |
| { | |
| "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: | |
| ```json | |
| { | |
| "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`): | |
| ```python | |
| 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): | |
| ```python | |
| 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: | |
| ```python | |
| 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: | |
| - **Swagger UI**: http://localhost:8000/docs | |
| - **ReDoc**: http://localhost:8000/redoc | |
| - **OpenAPI JSON**: http://localhost:8000/openapi.json | |
| --- | |
| ## Client Libraries | |
| ### Python Client Example | |
| ```python | |
| 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 | |
| - **Core Services**: [CORE_SERVICES.md](CORE_SERVICES.md) | |
| - **Data Flow**: [../DATA_FLOW.md](../DATA_FLOW.md) | |
| - **Frontend Components**: [../frontend/COMPONENTS.md](../frontend/COMPONENTS.md) | |