NaturalAPI is a REST server (powered by FastAPI) that uses Agentic CLI tools 🤖 (Codex, ClaudeCode, GitHub Copilot) to implement server-side logic expressed in Natural Language.
An endpoint is defined as a Markdown document, here is the source of chuck-norris.md:
---
route: /chuck
---
Tell a Chuck Norris joke.You can try the result of this "logic" at:
http://localhost:1337/chuck
The API surface is build dynamically from the markdown documents you provide, and a OpenAPI compliant API documentation is provided at:
- Data: http://localhost:1337/openapi.json
- Swagger: http://localhost:1337/openapi
- Python 3.11
- Codex CLI (optional, for AI functionality)
- Clone the repository
git clone git@github.com:marcopeg/natural-api.git
cd natural-api- Create and activate virtual environment:
python3.11 -m venv venv
source venv/bin/activate # On macOS/Linux- Install dependencies:
pip install -r requirements.txt- Start the server:
# Manual
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
# Makefile
make start- Test it:
# Get API info
curl http://localhost:1337/
# Try a dynamic prompt (fallback route)
curl http://localhost:1337/hi
# Try a prompt with path parameters
curl http://localhost:1337/greet/AliceThe server automatically maps HTTP requests to AI prompts stored in data/projects/{project}/prompts/*.md.
Create a Markdown file in your project's prompts directory (e.g., data/projects/default/prompts/):
Example: data/projects/default/prompts/greet.md
---
route: /greet/{name}
verb: GET
model: gpt-5.1-codex-mini
---
Generate a warm, personalized greeting for ${name}. Make it friendly and creative.---
route: /path/{param} # Optional: Explicit route (supports path parameters)
method: GET # Optional: HTTP method (default: GET)
model: gpt-5.1-codex # Optional: LLM model override
agent: codex # Optional: AI provider override
dry: true # Optional: Enable dry-run mode for this prompt
body: # Optional: Request body schema (POST/PUT/PATCH only)
field_name:
type: string # Types: string, number, integer, boolean
required: true # Optional: default is false
default: "value" # Optional: default value
minLength: 3 # Optional: string constraints
maxLength: 100
pattern: "^[A-Z]" # Optional: regex pattern
enum: [val1, val2] # Optional: allowed values
min: 0 # Optional: number constraints
max: 100
description: "..." # Optional: field description
---The system uses two-tier matching:
-
Explicit Routes - Match prompts with
routeconfigurationroute: /greet/{name}matchesGET /greet/Alice- Supports path parameters:
{name},{id}, etc. - Supports path wildcards:
{path:path}(matches entire remaining path)
-
Fallback Routes - Match by filename
hi.mdautomatically createsGET /hi- Only matches GET requests
- Single path segment only (no slashes)
Use bash-style variables in prompt body:
Syntax:
${variable}- Simple substitution (empty string if not found)${variable:default}- Substitution with default value${route.variable}- Explicit route parameter (from URL path)${body.variable}- Request body field (from POST/PUT/PATCH JSON)
Variables come from:
- Path parameters (e.g.,
{name}in route becomes${name}or${route.name}) - Request body fields (e.g., JSON
{"text": "hello"}becomes${body.text}) - Future: Query parameters (
?role=admin→${query.role})
Example with path parameters:
---
route: /user/{username}/profile
---
Create a profile for ${username} with occupation ${occupation:Software Developer}.Request GET /user/alice/profile → Prompt: "Create a profile for alice with occupation Software Developer."
Example with request body:
---
route: /analyze
method: POST
body:
text:
type: string
required: true
sentiment:
type: string
enum: [positive, negative, neutral]
default: neutral
---
Analyze this text: "${body.text}"
Expected sentiment: ${body.sentiment}Request:
curl -X POST http://localhost:1337/analyze \
-H "Content-Type: application/json" \
-d '{"text": "This is amazing!"}'→ Prompt: "Analyze this text: "This is amazing!"\nExpected sentiment: neutral"
The project includes several example prompts in data/projects/default/prompts/:
hi.md- Simple greeting (fallback route:GET /hi)calc.md- Calculator (explicit route:GET /calculator)greet.md- Personalized greeting (path param:GET /greet/{name})analyze.md- Text analysis (POST with body validation)
Each project can have its own set of prompts. Create new projects by adding directories under data/projects/.
Core Routes:
GET /- API information and available providersGET /openapi.json?project={id}- OpenAPI specification for projectGET /openapi?project={id}- Swagger UI for interactive API testing
Dynamic Routes (prompt-based):
- Any path matching prompts in
data/projects/{project}/prompts/ - Controlled by
X-Project-Idheader (default:default) - Controlled by
X-User-Idheader (default:anonymous) - Examples:
GET /hi- Random greeting (fallback route fromhi.md)GET /calculator- Sum calculation (explicit route fromcalc.md)GET /greet/Alice- Personalized greeting with path parameterPOST /analyze/hello- Text sentiment analysis with request body
Dry-Run Mode:
- Add
?dry=truequery parameter orX-Dry: trueheader - Returns command preview without execution
- CLI clients get markdown, browsers get HTML
Configure the server using environment variables:
# Select AI provider (default: codex)
export AI_PROVIDER=codex
# Set workspace directory (default: ./data)
export WORKSPACE_DIR=./data
# Set command timeout in seconds (default: 60)
export TIMEOUT_SECONDS=60
# Set logging level (default: INFO)
export LOG_LEVEL=DEBUG
# Enable OpenAPI endpoint (default: true)
export OPENAPI_ENABLED=trueProject Isolation:
# Use specific project
curl http://localhost:1337/hi -H "X-Project-Id: myproject"
# Projects stored in: data/projects/{project-id}/User Workspaces:
# User-specific workspace
curl http://localhost:1337/hi -H "X-User-Id: alice" -H "X-Project-Id: myproject"
# Workspace: data/storage/{user-id}/{project-id}/Request Logging:
- All requests logged to:
data/logs/{YYYY}/{MM}/{DD}/YYYYMMDD-HHMM-SSμμμμμμ-{status}.md - Includes: request details, AI command, execution output, response
- Format: Structured markdown with all context
The project includes comprehensive test coverage:
1. Unit Tests (fast, mocked dependencies)
# Test individual modules
pytest tests/test_prompt_loader.py -v # Prompt loading & parsing
pytest tests/test_variables.py -v # Variable substitution
pytest tests/test_router.py -v # Route matching
pytest tests/test_executor.py -v # Prompt execution
pytest tests/test_providers.py -v # AI providers
pytest tests/test_main.py -v # API endpoints2. E2E Tests (real server, actual HTTP requests)
# Run E2E tests (starts real servers on different ports)
pytest tests/test_e2e.py -vRun all tests:
source venv/bin/activate
pytest tests/ -vQuick test (skip slow E2E tests):
pytest tests/ -v -k "not e2e_hello"With the server running:
# API info
curl http://localhost:1337/
# Dynamic prompts - fallback routes
curl http://localhost:1337/hi -H "X-Project-Id: default"
# Dynamic prompts - with path parameters
curl http://localhost:1337/greet/Alice -H "X-Project-Id: default"
# Dynamic prompts - POST requests with body
curl -X POST http://localhost:1337/analyze \
-H "Content-Type: application/json" \
-H "X-Project-Id: default" \
-d '{"text": "This is amazing!"}'
# Dry-run mode (preview command)
curl "http://localhost:1337/hi?dry=true" -H "X-Project-Id: default"
# OpenAPI/Swagger UI
open http://localhost:1337/openapi?project=default
# Multi-project
curl http://localhost:1337/hi -H "X-Project-Id: test"
# Multi-user
curl http://localhost:1337/hi -H "X-User-Id: alice" -H "X-Project-Id: default"
# Check logs
ls -lh data/logs/$(date +%Y/%m/%d)/# 1. Activate virtual environment
source venv/bin/activate
# 2. Start server with auto-reload
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
# 3. In another terminal, run tests
source venv/bin/activate
pytest tests/ -vConfigure the server using environment variables:
# Select AI provider (default: codex)
export AI_PROVIDER=codex
# Set workspace directory (default: ./data)
export WORKSPACE_DIR=./data
# Set command timeout in seconds (default: 60)
export TIMEOUT_SECONDS=60AIProvider (Abstract Base Class)
├── CodexProvider (Codex CLI)
├── ClaudeProvider (Future)
└── CopilotProvider (Future)
Current Providers:
codex- Wraps Codex CLI (codex execcommands)
Configuration-driven: Set AI_PROVIDER environment variable to switch providers.
HTTP Request
↓
DynamicRouter (load prompts from data/prompts/*.md)
↓
RouteMatch (explicit route or fallback filename)
↓
PromptExecutor (substitute variables, execute AI)
↓
AIProviderResult (return raw output)
Key Components:
- PromptLoader - Parses Markdown files with YAML frontmatter
- DynamicRouter - Matches requests to prompts (explicit then fallback)
- VariableSubstitution - Replaces
${var}with path parameters - PromptExecutor - Runs AI provider with processed prompt
Comprehensive HTTP status codes:
200- Success404- Not Found (no matching prompt)408- Request Timeout (AI command exceeded timeout)500- Internal Server Error (AI command failed)503- Service Unavailable (AI provider not configured/installed)
All errors return JSON with details:
{
"detail": {
"error": "Error type",
"message": "Detailed message",
"provider": "codex"
}
}natural-api/
├── src/
│ ├── main.py # FastAPI application with dynamic routing
│ ├── config.py # Configuration management (multi-project/user)
│ ├── prompts/
│ │ ├── loader.py # Prompt file loading & parsing
│ │ ├── router.py # Dynamic route matching
│ │ ├── variables.py # Variable substitution
│ │ ├── executor.py # Prompt execution with AI
│ │ └── body_validator.py # Request body schema validation
│ ├── providers/
│ │ ├── base.py # AIProvider abstract base class
│ │ ├── codex.py # CodexProvider implementation
│ │ └── factory.py # ProviderFactory
│ ├── logging/
│ │ ├── models.py # LogEntry dataclass
│ │ ├── timestamp.py # Timestamp utilities
│ │ ├── formatter.py # Markdown log formatter
│ │ ├── html_formatter.py # HTML formatter for browsers
│ │ ├── writer.py # Log file writer
│ │ └── context.py # Request logging context
│ └── openapi/
│ └── generator.py # OpenAPI spec generation
├── tests/
│ ├── test_main.py # API endpoint tests
│ ├── test_config.py # Config tests (projects/users)
│ ├── test_prompt_loader.py # Prompt loading tests
│ ├── test_variables.py # Variable substitution tests
│ ├── test_router.py # Route matching tests
│ ├── test_executor.py # Prompt execution tests
│ ├── test_providers.py # Provider tests
│ ├── test_body_validator.py # Request body validation tests
│ ├── test_logging_*.py # Logging system tests (27 tests)
│ ├── test_openapi_*.py # OpenAPI tests
│ ├── test_e2e.py # E2E tests (real server)
│ └── e2e_utils.py # E2E utilities
├── data/
│ ├── projects/ # Multi-project support
│ │ ├── default/
│ │ │ ├── AGENTS.md # Project-level AI instructions
│ │ │ └── prompts/ # Project-specific prompts
│ │ │ ├── hi.md
│ │ │ ├── calc.md
│ │ │ ├── greet.md
│ │ │ └── analyze.md
│ │ └── test/
│ │ └── prompts/
│ ├── storage/ # User workspaces per project
│ │ ├── anonymous/
│ │ │ └── default/ # User workspace
│ │ └── alice/
│ │ └── myproject/
│ └── logs/ # Request logs
│ └── {YYYY}/{MM}/{DD}/
│ └── YYYYMMDD-HHMM-SSμμμμμμ-{status}.md
└── requirements.txt # Python dependencies
MIT