Merge _configs/ into config/ for single configuration directory. Update all path references. Changes: - Move _configs/* to config/ - Update .gitignore for new patterns - No code references to _configs/ found Impact: -1 root directory (layout_conventions.md compliance)
6.6 KiB
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:
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:
clap = { version = "4.4", features = ["derive"] }
In src/main.rs:
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
// 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
if cfg.cors_enabled {
router = router.layer(CorsLayer::permissive());
tracing::debug!("CORS enabled");
}
5. Config-Driven Static Files
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:
# <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:
#!/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
--configCLI 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.tomlin project rootscripts/install-<tool>-api.shREADME.mdwith 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
- Configuration First:
--configis not optional - Be Explicit: No defaults, no magic
- Be Kind: Clear error messages with file paths
- Be Flexible: Conditional features (CORS, TLS, files)
- Be Portable: No hardcoded paths anywhere
- Be Documented: Template + comments + installation guide