261 lines
6.6 KiB
Markdown
261 lines
6.6 KiB
Markdown
|
|
# API Server Development Skill
|
||
|
|
|
||
|
|
**Purpose**: Provide guidance for creating and reviewing production-grade REST API servers that follow Project Agnostic Practice (PAP) principles.
|
||
|
|
|
||
|
|
**When to Use**:
|
||
|
|
- Creating a new REST API server
|
||
|
|
- Reviewing existing API servers for compliance
|
||
|
|
- Implementing configuration systems
|
||
|
|
- Adding CORS/TLS/static files support
|
||
|
|
|
||
|
|
## The Config-Driven API Server Pattern
|
||
|
|
|
||
|
|
This skill is based on the **syntaxis-api** implementation, which establishes the standard pattern for all API servers in this project.
|
||
|
|
|
||
|
|
### What Makes This Pattern Special
|
||
|
|
|
||
|
|
✅ **Portable**: No hardcoded paths - works anywhere
|
||
|
|
✅ **Explicit**: All configuration required, no defaults
|
||
|
|
✅ **Safe**: Clear error messages, validation
|
||
|
|
✅ **Flexible**: Config-driven CORS, logging, static files, TLS
|
||
|
|
✅ **Graceful**: Missing optional features don't break server
|
||
|
|
|
||
|
|
## Quick Start for New API Servers
|
||
|
|
|
||
|
|
### 1. Configuration Module
|
||
|
|
|
||
|
|
Create `src/config.rs`:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum ConfigError {
|
||
|
|
#[error("IO error: {0}")]
|
||
|
|
Io(#[from] std::io::Error),
|
||
|
|
#[error("TOML parsing error: {0}")]
|
||
|
|
TomlError(#[from] toml::de::Error),
|
||
|
|
#[error("Configuration error: {0}")]
|
||
|
|
Custom(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct ServerConfig {
|
||
|
|
pub host: String,
|
||
|
|
pub port: u16,
|
||
|
|
pub database_path: String,
|
||
|
|
pub public_files_path: String,
|
||
|
|
pub cors_enabled: bool,
|
||
|
|
pub log_level: String,
|
||
|
|
#[serde(default)]
|
||
|
|
pub tls: Option<TlsConfig>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct TlsConfig {
|
||
|
|
pub enabled: bool,
|
||
|
|
pub cert_path: Option<String>,
|
||
|
|
pub key_path: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ServerConfig {
|
||
|
|
pub fn load<P: AsRef<std::path::Path>>(config_path: P) -> Result<Self, ConfigError> {
|
||
|
|
// Extract [server] section explicitly
|
||
|
|
// Fail if any required field missing
|
||
|
|
// Return clear error messages
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn socket_addr(&self) -> Result<SocketAddr, ConfigError> { ... }
|
||
|
|
pub fn database_path_buf(&self) -> PathBuf { ... }
|
||
|
|
pub fn public_files_path_buf(&self) -> PathBuf { ... }
|
||
|
|
pub fn validate_tls(&self) -> Result<(), ConfigError> { ... }
|
||
|
|
pub fn log_config(&self) { ... }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. CLI Argument Parsing
|
||
|
|
|
||
|
|
Add to `Cargo.toml`:
|
||
|
|
```toml
|
||
|
|
clap = { version = "4.4", features = ["derive"] }
|
||
|
|
```
|
||
|
|
|
||
|
|
In `src/main.rs`:
|
||
|
|
```rust
|
||
|
|
use clap::Parser;
|
||
|
|
|
||
|
|
#[derive(Parser)]
|
||
|
|
#[command(name = "tool-server")]
|
||
|
|
struct Args {
|
||
|
|
/// Path to configuration file (TOML format)
|
||
|
|
#[arg(short, long)]
|
||
|
|
config: PathBuf,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::main]
|
||
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||
|
|
let args = Args::parse();
|
||
|
|
let cfg = ServerConfig::load(&args.config)?;
|
||
|
|
cfg.validate_tls()?;
|
||
|
|
// ... rest of initialization
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Tracing from Config
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Initialize tracing with configured log level
|
||
|
|
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||
|
|
.or_else(|_| tracing_subscriber::EnvFilter::try_new(&cfg.log_level))?;
|
||
|
|
|
||
|
|
tracing_subscriber::fmt()
|
||
|
|
.with_env_filter(env_filter)
|
||
|
|
.init();
|
||
|
|
|
||
|
|
cfg.log_config();
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Conditional CORS
|
||
|
|
|
||
|
|
```rust
|
||
|
|
if cfg.cors_enabled {
|
||
|
|
router = router.layer(CorsLayer::permissive());
|
||
|
|
tracing::debug!("CORS enabled");
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Config-Driven Static Files
|
||
|
|
|
||
|
|
```rust
|
||
|
|
let public_files_path = cfg.public_files_path_buf();
|
||
|
|
let public_files_exist = public_files_path.exists();
|
||
|
|
|
||
|
|
if public_files_exist {
|
||
|
|
tracing::info!("Static files directory: {}", public_files_path.display());
|
||
|
|
} else {
|
||
|
|
tracing::warn!(
|
||
|
|
"Static files directory does not exist: {}. Skipping /public and /docs endpoints.",
|
||
|
|
public_files_path.display()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if public_files_exist {
|
||
|
|
router = router
|
||
|
|
.nest_service("/public", ServeDir::new(public_files_path.clone()))
|
||
|
|
.nest_service("/docs", ServeDir::new(public_files_path));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6. Template Configuration File
|
||
|
|
|
||
|
|
Create `<tool>-api-config.template.toml`:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
# <Tool> API Server Configuration Template
|
||
|
|
#
|
||
|
|
# Usage: <tool>-server --config /path/to/config.toml
|
||
|
|
|
||
|
|
[server]
|
||
|
|
host = "127.0.0.1"
|
||
|
|
port = 3000
|
||
|
|
database_path = "./data/database.db"
|
||
|
|
public_files_path = "./public"
|
||
|
|
cors_enabled = true
|
||
|
|
log_level = "info"
|
||
|
|
|
||
|
|
[server.tls]
|
||
|
|
enabled = false
|
||
|
|
# cert_path = "/path/to/cert.pem"
|
||
|
|
# key_path = "/path/to/key.pem"
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7. Installation Script
|
||
|
|
|
||
|
|
Create `scripts/install-<tool>-api.sh`:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
#!/bin/bash
|
||
|
|
set -e
|
||
|
|
|
||
|
|
CONFIG_DIR="$HOME/.config/<tool>-api"
|
||
|
|
BINARY="${PROJECT_ROOT}/target/release/<tool>-server"
|
||
|
|
|
||
|
|
mkdir -p "$CONFIG_DIR"
|
||
|
|
cp <tool>-api-config.template.toml "$CONFIG_DIR/<tool>-api-config.toml"
|
||
|
|
|
||
|
|
cat > "$CONFIG_DIR/run-<tool>-server.sh" << 'EOF'
|
||
|
|
#!/bin/bash
|
||
|
|
exec BINARY_PATH --config CONFIG_FILE_PATH "$@"
|
||
|
|
EOF
|
||
|
|
|
||
|
|
sed -i '' "s|BINARY_PATH|$BINARY|g" "$CONFIG_DIR/run-<tool>-server.sh"
|
||
|
|
sed -i '' "s|CONFIG_FILE_PATH|$CONFIG_DIR/<tool>-api-config.toml|g" "$CONFIG_DIR/run-<tool>-server.sh"
|
||
|
|
chmod +x "$CONFIG_DIR/run-<tool>-server.sh"
|
||
|
|
|
||
|
|
echo "✅ Installation complete!"
|
||
|
|
echo "Run: $CONFIG_DIR/run-<tool>-server.sh"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Review Checklist
|
||
|
|
|
||
|
|
When reviewing an existing API server:
|
||
|
|
|
||
|
|
### Configuration System
|
||
|
|
- [ ] Has `--config` CLI argument (clap)
|
||
|
|
- [ ] Loads TOML from file
|
||
|
|
- [ ] Extracts `[server]` table explicitly
|
||
|
|
- [ ] No hardcoded paths
|
||
|
|
- [ ] No env var overrides
|
||
|
|
- [ ] All fields required (no defaults)
|
||
|
|
- [ ] Clear error messages
|
||
|
|
|
||
|
|
### Configuration Fields
|
||
|
|
- [ ] `host` (string)
|
||
|
|
- [ ] `port` (u16)
|
||
|
|
- [ ] `database_path` (string)
|
||
|
|
- [ ] `public_files_path` (string)
|
||
|
|
- [ ] `cors_enabled` (bool)
|
||
|
|
- [ ] `log_level` (string)
|
||
|
|
- [ ] `[server.tls]` (optional)
|
||
|
|
|
||
|
|
### Features
|
||
|
|
- [ ] Log level applied to tracing
|
||
|
|
- [ ] CORS conditional based on config
|
||
|
|
- [ ] Static files conditional (with existence check)
|
||
|
|
- [ ] TLS validation if enabled
|
||
|
|
- [ ] Configuration logged at startup
|
||
|
|
|
||
|
|
### Files
|
||
|
|
- [ ] `<tool>-api-config.template.toml` in project root
|
||
|
|
- [ ] `scripts/install-<tool>-api.sh`
|
||
|
|
- [ ] `README.md` with configuration examples
|
||
|
|
- [ ] Tests for config loading
|
||
|
|
|
||
|
|
## Reference Implementation
|
||
|
|
|
||
|
|
**Location**: `crates/syntaxis-api/` in syntaxis
|
||
|
|
|
||
|
|
Review this crate for a complete, production-ready example:
|
||
|
|
- Configuration: `src/config.rs`
|
||
|
|
- Server setup: `src/main.rs`
|
||
|
|
- Template: `syntaxis-api-config.template.toml`
|
||
|
|
- Installation: `scripts/install-syntaxis-api.sh`
|
||
|
|
|
||
|
|
## Full Documentation
|
||
|
|
|
||
|
|
See **API_SERVER_GUIDELINE.md** in project root for:
|
||
|
|
- Complete principles
|
||
|
|
- Detailed implementation checklist
|
||
|
|
- Error handling standards
|
||
|
|
- All required patterns
|
||
|
|
|
||
|
|
## Key Takeaways
|
||
|
|
|
||
|
|
1. **Configuration First**: `--config` is not optional
|
||
|
|
2. **Be Explicit**: No defaults, no magic
|
||
|
|
3. **Be Kind**: Clear error messages with file paths
|
||
|
|
4. **Be Flexible**: Conditional features (CORS, TLS, files)
|
||
|
|
5. **Be Portable**: No hardcoded paths anywhere
|
||
|
|
6. **Be Documented**: Template + comments + installation guide
|