Axumite.

Version: 0.1.0
Author: ProDeSquare
License: GNU General Public License v3.0 (GPL-3.0)
Website: https://prodesquare.com
Contact: hamza@prodesquare.com
GitHub: https://github.com/ProDeSquare/axumite

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

πŸ’Ύ Database & Caching

πŸ›‘οΈ Rate Limiting & CORS

πŸ“Š Observability & Performance

πŸ”§ Production Optimizations


Architecture

Project Structure

axumite/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main.rs              # Entry point
β”‚   β”œβ”€β”€ app.rs               # Application initialization & server setup
β”‚   β”œβ”€β”€ config.rs            # Configuration management
β”‚   β”œβ”€β”€ state.rs             # Shared application state
β”‚   β”œβ”€β”€ error.rs             # Error handling
β”‚   β”œβ”€β”€ controllers/         # Request handlers
β”‚   β”‚   β”œβ”€β”€ mod.rs
β”‚   β”‚   β”œβ”€β”€ root_controller.rs
β”‚   β”‚   β”œβ”€β”€ health_controller.rs
β”‚   β”‚   └── error_controller.rs
β”‚   β”œβ”€β”€ routes/              # Route definitions
β”‚   β”‚   β”œβ”€β”€ mod.rs
β”‚   β”‚   └── health_routes.rs
β”‚   β”œβ”€β”€ models/              # Data models
β”‚   β”‚   β”œβ”€β”€ mod.rs
β”‚   β”‚   └── health_model.rs
β”‚   β”œβ”€β”€ middleware/          # Custom middleware
β”‚   β”‚   β”œβ”€β”€ mod.rs
β”‚   β”‚   β”œβ”€β”€ rate_limit.rs
β”‚   β”‚   └── cors.rs
β”‚   └── db/                  # Database connections
β”‚       β”œβ”€β”€ mod.rs
β”‚       β”œβ”€β”€ postgres.rs
β”‚       └── redis.rs
β”œβ”€β”€ Cargo.toml
└── .env.example

Middleware Stack

Requests flow through the following middleware layers (in order):

  1. Rate Limiter - Enforces request rate limits based on environment
  2. Trace Layer - Logs request/response information
  3. CORS Layer - Handles cross-origin resource sharing policies
  4. Compression Layer - Compresses responses when supported by client
  5. 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>,      // PostgreSQL connection pool
    pub redis: Arc<RedisPool>,     // Redis connection manager
    pub config: Arc<AppConfig>,    // Application configuration
}

State is cloned efficiently using Arc for thread-safe shared access.


Getting Started

Prerequisites

Installation

  1. Clone or copy the Axumite framework:

    git clone git@github.com:ProDeSquare/axumite.git
    cd axumite
    
  2. Copy the environment configuration:

    cp .env.example .env
    
  3. Configure your environment variables:

    # Application Settings
    APP_NAME="axumite"
    APP_HOST="127.0.0.1"
    APP_PORT="8080"
    APP_ENV="development"  # or "production"
    
    # PostgreSQL Configuration
    DATABASE_HOST="127.0.0.1"
    DATABASE_PORT="5432"
    DATABASE_USER="postgres"
    DATABASE_PASS="your_password"
    DATABASE_NAME="prodesquare"
    
    # CORS Configuration
    ALLOWED_ORIGINS="https://prodesquare.com,https://api.prodesquare.com"
    
    # Redis Configuration
    REDIS_URL="redis://127.0.0.1:6379"
    
    # Logging Configuration
    RUST_LOG="axumite=debug,tower_http=info"
    
  4. Build and run:

    # Development
    cargo run
    
    # Production (optimized)
    cargo build --release
    ./target/release/axumite
    

Configuration Reference

Environment Variables

VariableRequiredDefaultDescription
APP_NAMENoprodesquare_apiApplication name (used in logs)
APP_HOSTNo127.0.0.1Host address to bind
APP_PORTNo8080Port to listen on
APP_ENVNoproductionEnvironment mode (development or production)
DATABASE_HOSTNo127.0.0.1PostgreSQL host
DATABASE_PORTNo5432PostgreSQL port
DATABASE_USERNopostgresPostgreSQL username
DATABASE_PASSNo""PostgreSQL password
DATABASE_NAMENoprodesquarePostgreSQL database name
REDIS_URLYes-Redis connection URL
ALLOWED_ORIGINSNononeCORS policy: none (disabled), * (all origins), or comma-separated list
RUST_LOGNo-Log level configuration

Rate Limiting Configuration

Rate limiting behavior is controlled in src/middleware/rate_limit.rs:

const RATE_LIMITER_PER_SECOND: u64 = 10;  // Requests per second
const RATE_LIMITER_BURST_SIZE: u32 = 20;  // Burst capacity

Development Mode (APP_ENV=development):

Production Mode (APP_ENV=production):

CORS Configuration

CORS behavior is controlled via the ALLOWED_ORIGINS environment variable:

Disabled (default):

ALLOWED_ORIGINS="none"
# or
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:


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

  1. 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))
    }
    
  2. Export in src/controllers/mod.rs:

    pub mod user_controller;
    

Creating a New Route Module

  1. Create route file in src/routes/:

    // src/routes/user_routes.rs
    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))
    }
    
  2. 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

// src/models/user_model.rs
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();

    // Set a value
    conn.set::<_, _, ()>("key", "value").await?;

    // Get a value
    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> {
    // Any error implementing Into<anyhow::Error> can be used
    let data = fetch_data().await?;
    Ok(Json(data))
}

All errors are:

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:

# Debug everything
RUST_LOG=debug

# Specific crates
RUST_LOG=axumite=debug,tower_http=info,sqlx=warn

# Production (info and above)
RUST_LOG=info

Log Output

Trace Spans

Every HTTP request automatically creates a trace span with:


Production Deployment

Building for Production

# Build optimized binary
cargo build --release

# Binary location
./target/release/axumite

The release profile includes:

Environment Setup

  1. Set APP_ENV=production to enable IP-based rate limiting
  2. Configure RUST_LOG=info or RUST_LOG=warn for production logging
  3. Use strong database passwords
  4. Consider using Redis with authentication
  5. 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

CrateVersionPurpose
axum0.8.6Web framework
tower0.5.2Middleware infrastructure
tower-http0.6.6HTTP middleware (tracing, compression)
tower_governor0.8.0Rate limiting middleware
governor0.10.1Rate limiting algorithm
tokio1.48.0Async runtime (full features)
deadpool-postgres0.14.1PostgreSQL connection pooling
tokio-postgres0.7.15Async PostgreSQL driver
redis0.32.7Redis client (tokio-compatible)
serde1.0.228Serialization framework
serde_json1.0.145JSON support
anyhow1.0.100Error handling
tracing0.1.41Instrumentation framework
tracing-subscriber0.3.20Tracing output formatting
dotenvy0.15.7Environment 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:


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:

5. Validate input

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"

"Failed to connect to Redis"

Rate limiting not working as expected

High memory usage


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: