Overview
Axumite is a lightweight, high-performance API framework built on top of Axum and Tower. It provides a structured, production-ready foundation for building database-backed web APIs in Rust with built-in rate limiting, connection pooling, distributed caching, request tracing, and graceful shutdown capabilities.
Originally crafted for personal use, Axumite embodies a preferred architecture for service designβsimple, predictable, and efficient. Anyone is free to use, modify, or extend it for both personal and commercial projects under the GPL-3.0 license.
Core Features
π Framework & Routing
- Built on Axum 0.8.6 - Leverages Axum's ergonomic routing and handler system
- Tower Middleware Integration - Composable middleware stack using Tower 0.5.2
- Structured Route Organization - Modular route definitions with easy extensibility
- Custom Error Handling - Centralized error handling with proper logging
- 404 Fallback Handler - Graceful handling of unknown routes with JSON responses
πΎ Database & Caching
- PostgreSQL Connection Pooling - Powered by Deadpool (deadpool-postgres 0.14.1)
- Configurable pool size (default: 16 connections)
- Fast recycling method for optimal performance
- Automatic connection health checks
- Redis Connection Manager - Async Redis integration (redis 0.32.7)
- Connection manager for efficient connection reuse
- Built-in connection recovery
- Tokio-compatible async operations
π‘οΈ Rate Limiting & CORS
- Environment-Aware Rate Limiting - Automatic configuration based on environment
- Development Mode: Global key extractor (no IP tracking)
- Production Mode: Smart IP key extractor with real IP detection
- Configurable Limits:
- 10 requests per second per key
- Burst capacity of 20 requests
- Built with Governor & Tower-Governor - Industry-standard rate limiting
- Flexible CORS Configuration - Environment-Aware Rate Limiting
- Disable CORS entirely for internal APIs
- Allow all origins for development (
*) - Whitelist specific origins for production (comma-separated)
- Automatic credentials support for whitelisted origins
- Distributed Tracing - Built-in request tracing with tracing 0.1.41
- Configurable log levels via
RUST_LOG environment variable - Request/response logging with Tower HTTP trace layer
- Automatic span creation for all HTTP requests
- Response Compression - Full compression support (gzip, br, deflate, zstd)
- Environment-Based Configuration - All settings via environment variables
π§ Production Optimizations
- Optimized Release Profile:
- Link-Time Optimization (LTO) enabled
- Single codegen unit for maximum optimization
- Panic abort for smaller binary size
- Debug symbols stripped
- Graceful Shutdown - Proper signal handling (Ctrl+C) with connection draining
- Health Check Endpoint - Built-in
/health endpoint for load balancers
Architecture
Project Structure
axumite/
βββ src/
β βββ main.rs
β βββ app.rs
β βββ config.rs
β βββ state.rs
β βββ error.rs
β βββ controllers/
β β βββ mod.rs
β β βββ root_controller.rs
β β βββ health_controller.rs
β β βββ error_controller.rs
β βββ routes/
β β βββ mod.rs
β β βββ health_routes.rs
β βββ models/
β β βββ mod.rs
β β βββ health_model.rs
β βββ middleware/
β β βββ mod.rs
β β βββ rate_limit.rs
β β βββ cors.rs
β βββ db/
β βββ mod.rs
β βββ postgres.rs
β βββ redis.rs
βββ Cargo.toml
βββ .env.example
Middleware Stack
Requests flow through the following middleware layers (in order):
- Rate Limiter - Enforces request rate limits based on environment
- Trace Layer - Logs request/response information
- CORS Layer - Handles cross-origin resource sharing policies
- Compression Layer - Compresses responses when supported by client
- Router - Routes requests to appropriate handlers
Application State
The AppState struct is shared across all handlers and contains:
pub struct AppState {
pub db_pool: Arc<DbPool>,
pub redis: Arc<RedisPool>,
pub config: Arc<AppConfig>,
}
State is cloned efficiently using Arc for thread-safe shared access.
Getting Started
Prerequisites
- Rust 1.80+ (2024 edition)
- PostgreSQL 12+
- Redis 6+
Installation
Clone or copy the Axumite framework:
git clone git@github.com:ProDeSquare/axumite.git
cd axumite
Copy the environment configuration:
cp .env.example .env
Configure your environment variables:
APP_NAME="axumite"
APP_HOST="127.0.0.1"
APP_PORT="8080"
APP_ENV="development" # or "production"
DATABASE_HOST="127.0.0.1"
DATABASE_PORT="5432"
DATABASE_USER="postgres"
DATABASE_PASS="your_password"
DATABASE_NAME="prodesquare"
ALLOWED_ORIGINS="https://prodesquare.com,https://api.prodesquare.com"
REDIS_URL="redis://127.0.0.1:6379"
RUST_LOG="axumite=debug,tower_http=info"
Build and run:
cargo run
cargo build --release
./target/release/axumite
Configuration Reference
Environment Variables
| Variable | Required | Default | Description |
|---|
APP_NAME | No | prodesquare_api | Application name (used in logs) |
APP_HOST | No | 127.0.0.1 | Host address to bind |
APP_PORT | No | 8080 | Port to listen on |
APP_ENV | No | production | Environment mode (development or production) |
DATABASE_HOST | No | 127.0.0.1 | PostgreSQL host |
DATABASE_PORT | No | 5432 | PostgreSQL port |
DATABASE_USER | No | postgres | PostgreSQL username |
DATABASE_PASS | No | "" | PostgreSQL password |
DATABASE_NAME | No | prodesquare | PostgreSQL database name |
REDIS_URL | Yes | - | Redis connection URL |
ALLOWED_ORIGINS | No | none | CORS policy: none (disabled), * (all origins), or comma-separated list |
RUST_LOG | No | - | Log level configuration |
Rate Limiting Configuration
Rate limiting behavior is controlled in src/middleware/rate_limit.rs:
const RATE_LIMITER_PER_SECOND: u64 = 10;
const RATE_LIMITER_BURST_SIZE: u32 = 20;
Development Mode (APP_ENV=development):
- Uses
GlobalKeyExtractor - all requests share the same rate limit - Useful for local testing without IP-based restrictions
Production Mode (APP_ENV=production):
- Uses
SmartIpKeyExtractor - rate limits per client IP - Automatically detects real IP from proxy headers
CORS Configuration
CORS behavior is controlled via the ALLOWED_ORIGINS environment variable:
Disabled (default):
ALLOWED_ORIGINS="none"
ALLOWED_ORIGINS=""
No cross-origin requests allowed. Use for internal APIs.
All Origins (development only):
ALLOWED_ORIGINS="*"
Allows all origins with any headers. Not recommended for production.
Specific Origins (production):
ALLOWED_ORIGINS="https://app.example.com,https://admin.example.com"
Comma-separated list of allowed origins. Automatically enables:
- Credentials support (
Access-Control-Allow-Credentials: true) - Standard headers:
Authorization, Content-Type, Accept - Standard methods:
GET, POST, PUT, PATCH, DELETE
Built-in Endpoints
Root Endpoint
GET /
Returns a simple status message.
Response:
axumite - alive and listening
Health Check
GET /health
Returns application health status. Verifies database and Redis connectivity.
Response (200 OK):
{
"status": "ok",
"version": "0.1.0"
}
404 Not Found
All unmatched routes return a structured error response.
Response (404 Not Found):
{
"status": 404,
"message": "No route found for '/unknown/path'"
}
Adding New Features
Creating a New Controller
Create controller file in src/controllers/:
// src/controllers/user_controller.rs
use axum::{extract::State, Json};
use crate::{error::AppError, state::AppState};
pub async fn get_users(
State(state): State<AppState>
) -> Result<Json<Vec<String>>, AppError> {
// Use state.db_pool, state.redis, state.config
let users = vec!["Alice".to_string(), "Bob".to_string()];
Ok(Json(users))
}
Export in src/controllers/mod.rs:
pub mod user_controller;
Creating a New Route Module
Create route file in src/routes/:
use axum::{Router, routing::get};
use crate::{controllers::user_controller, state::AppState};
pub fn router() -> Router<AppState> {
Router::new()
.route("/users", get(user_controller::get_users))
}
Register routes in src/routes/mod.rs:
mod user_routes;
pub fn create_router() -> Router<AppState> {
Router::new()
.route("/", get(root_handler))
.merge(health_routes::router())
.merge(user_routes::router()) // Add this line
.fallback(not_found_handler)
}
Creating a New Model
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
pub id: i32,
pub username: String,
pub email: String,
}
Don't forget to export in src/models/mod.rs.
Using the Database
use crate::{error::AppError, state::AppState};
use axum::extract::State;
pub async fn query_example(
State(state): State<AppState>
) -> Result<String, AppError> {
let client = state.db_pool.get().await?;
let rows = client
.query("SELECT name FROM users WHERE id = $1", &[&1])
.await?;
if let Some(row) = rows.first() {
let name: String = row.get(0);
Ok(name)
} else {
Ok("Not found".to_string())
}
}
Using Redis
use redis::AsyncCommands;
use crate::{error::AppError, state::AppState};
use axum::extract::State;
pub async fn redis_example(
State(state): State<AppState>
) -> Result<String, AppError> {
let mut conn = state.redis.clone();
conn.set::<_, _, ()>("key", "value").await?;
let value: String = conn.get("key").await?;
Ok(value)
}
Error Handling
Axumite uses a custom AppError type that wraps anyhow::Error:
use crate::error::AppError;
pub async fn handler() -> Result<Json<Data>, AppError> {
let data = fetch_data().await?;
Ok(Json(data))
}
All errors are:
- Automatically logged with full context
- Converted to HTTP 500 responses
- Returned as "Internal Server Error" to clients (details hidden for security)
For custom error responses, implement IntoResponse directly:
use axum::{http::StatusCode, response::IntoResponse, Json};
pub async fn handler() -> impl IntoResponse {
(StatusCode::BAD_REQUEST, Json(json!({"error": "Invalid input"})))
}
Logging & Tracing
Log Levels
Configure via RUST_LOG environment variable:
RUST_LOG=debug
RUST_LOG=axumite=debug,tower_http=info,sqlx=warn
RUST_LOG=info
Log Output
- INFO: Request/response logs, connection status
- DEBUG: Detailed application logic
- WARN: Non-critical issues (404s logged here)
- ERROR: Critical errors requiring attention
Trace Spans
Every HTTP request automatically creates a trace span with:
- Request method and path
- Response status code
- Request duration
Production Deployment
Building for Production
# Build optimized binary
cargo build --release
# Binary location
./target/release/axumite
The release profile includes:
- LTO (Link-Time Optimization): Maximum performance
- Single codegen unit: Better optimization across crate
- Panic = abort: Smaller binary size
- Stripped symbols: Reduced binary size
Environment Setup
- Set
APP_ENV=production to enable IP-based rate limiting - Configure
RUST_LOG=info or RUST_LOG=warn for production logging - Use strong database passwords
- Consider using Redis with authentication
- Deploy behind a reverse proxy (nginx, Caddy) for TLS
Reverse Proxy Configuration
When behind a proxy, ensure it sets the X-Forwarded-For header so the rate limiter can detect real client IPs.
Nginx example:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
Systemd Service
[Unit]
Description=Axumite API Service
After=network.target postgresql.service redis.service
[Service]
Type=simple
User=axumite
WorkingDirectory=/opt/axumite
EnvironmentFile=/opt/axumite/.env
ExecStart=/opt/axumite/axumite
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Dependencies
| Crate | Version | Purpose |
|---|
axum | 0.8.6 | Web framework |
tower | 0.5.2 | Middleware infrastructure |
tower-http | 0.6.6 | HTTP middleware (tracing, compression) |
tower_governor | 0.8.0 | Rate limiting middleware |
governor | 0.10.1 | Rate limiting algorithm |
tokio | 1.48.0 | Async runtime (full features) |
deadpool-postgres | 0.14.1 | PostgreSQL connection pooling |
tokio-postgres | 0.7.15 | Async PostgreSQL driver |
redis | 0.32.7 | Redis client (tokio-compatible) |
serde | 1.0.228 | Serialization framework |
serde_json | 1.0.145 | JSON support |
anyhow | 1.0.100 | Error handling |
tracing | 0.1.41 | Instrumentation framework |
tracing-subscriber | 0.3.20 | Tracing output formatting |
dotenvy | 0.15.7 | Environment variable loading |
Macros
apply_rate_limiter!
Conditionally applies the appropriate rate limiter based on environment.
Usage:
let app = apply_rate_limiter!(routes::create_router())
.with_state(state);
Behavior:
- In production: Applies IP-based rate limiter
- In development: Applies global rate limiter
Best Practices
1. Use AppError for handler errors
Return Result<T, AppError> from handlers to get automatic error logging and consistent error responses.
2. Share expensive resources via Arc
Database pools and Redis connections are already wrapped in Arc in AppState. Clone the state cheaply.
3. Keep handlers thin
Move business logic to separate service modules. Handlers should focus on HTTP concerns.
4. Use proper HTTP status codes
Return appropriate status codes with responses:
200 OK - Successful GET/PUT201 Created - Successful POST204 No Content - Successful DELETE400 Bad Request - Invalid input404 Not Found - Resource not found500 Internal Server Error - Server errors
Use Axum extractors and validation libraries to ensure data integrity before processing.
6. Use connection pools efficiently
Acquire database connections late and release them early. Don't hold connections across await points unnecessarily.
7. Monitor rate limit hits
In production, monitor rate limit rejections to identify potential issues or adjust limits.
Troubleshooting
"Database pool creation failed"
- Verify PostgreSQL is running and accessible
- Check
DATABASE_* environment variables - Ensure database exists and credentials are correct
"Failed to connect to Redis"
- Verify Redis is running
- Check
REDIS_URL format: redis://host:port - Test connection with
redis-cli ping
Rate limiting not working as expected
- Verify
APP_ENV is set correctly - Check proxy headers if behind reverse proxy
- Review rate limiter constants in
rate_limit.rs
High memory usage
- Reduce
max_size in database pool configuration - Check for connection leaks (not releasing pool connections)
- Monitor active connections in PostgreSQL
License
Axumite is licensed under the GNU General Public License v3.0 (GPL-3.0).
You are free to use, modify, and distribute this software, provided that derivative works remain open source under the same license.
For the full license text, see: LICENSE
Support
For questions, issues, or contributions: