axum template to start develop
Find a file Use this template
Jonatas Oliveira c77f4bd1c0
Some checks are pending
ci/woodpecker/push/pipeline/1 Pipeline was successful
ci/woodpecker/push/pipeline/2 Pipeline was successful
ci/woodpecker/cron/pipeline/1 Pipeline is pending
ci/woodpecker/cron/pipeline/2 Pipeline is pending
chore: add woodpecker badge
2026-01-03 16:48:17 +01:00
.woodpecker refactor: separe in more modules and add coverage process 2026-01-03 16:16:35 +01:00
hurl feat: add simple example with endpoints with axum 2026-01-01 20:02:31 +01:00
scripts refactor: separe in more modules and add coverage process 2026-01-03 16:16:35 +01:00
src refactor: separe in more modules and add coverage process 2026-01-03 16:16:35 +01:00
.gitignore feat: add simple example with endpoints with axum 2026-01-01 20:02:31 +01:00
Cargo.toml refactor: separe in more modules and add coverage process 2026-01-03 16:16:35 +01:00
LICENSE Initial commit 2026-01-01 14:13:21 +00:00
README.md chore: add woodpecker badge 2026-01-03 16:48:17 +01:00

status-badge

Axum Template

A production-ready Axum web application template with comprehensive error handling, middleware, and testing.

Features

  • Comprehensive Error Handling: Custom error types with proper HTTP status codes
  • Middleware System: 404 and 500 error handlers with detailed logging
  • API Documentation: Automatic OpenAPI/Swagger UI generation
  • Testing: Unit and integration tests with high coverage
  • Type Safety: Full Rust type safety with proper error propagation
  • Logging: Structured logging with tracing
  • Timeout Protection: Request timeout middleware
  • CORS Support: Configurable CORS headers

Project Structure

axum-template/
├── src/
│   ├── main.rs          # Application entry point
│   ├── lib.rs           # Library exports and router configuration
│   ├── errors.rs        # Error types and handling
│   └── handles.rs       # Middleware handlers (404, 500, etc.)
├── tests/
│   └── integration_test.rs  # Integration tests
└── Cargo.toml

Architecture

Error Module (errors.rs)

The error module provides a comprehensive error handling system with:

AppError Enum

All application errors are represented by the AppError enum:

pub enum AppError {
    NotFound(String),           // 404 - Resource not found
    BadRequest(String),         // 400 - Bad request
    Unauthorized(String),       // 401 - Unauthorized
    Forbidden(String),          // 403 - Forbidden
    Conflict(String),           // 409 - Conflict
    UnprocessableEntity(String),// 422 - Unprocessable entity
    InternalServer(String),     // 500 - Internal server error
    ServiceUnavailable(String), // 503 - Service unavailable
    Database(String),           // 500 - Database error
    Validation(String),         // 400 - Validation error
}

ErrorResponse Structure

All errors return a consistent JSON structure:

{
  "status": 404,
  "error": "NOT_FOUND",
  "message": "Resource not found: Todo with id 999 not found"
}

Automatic Conversions

The error module provides automatic conversions from common error types:

  • anyhow::ErrorAppError::InternalServer
  • sqlx::ErrorAppError::Database or AppError::NotFound
  • serde_json::ErrorAppError::BadRequest
  • std::io::ErrorAppError::InternalServer

Handles Module (handles.rs)

The handles module provides middleware for error handling:

404 Not Found Handler

// Basic 404 handler
handle_404()

// 404 handler with path information
handle_404_with_path(req)

Returns:

{
  "status": 404,
  "error": "NOT_FOUND",
  "message": "The requested resource was not found",
  "path": "/api/users/999"
}

500 Internal Server Error Handler

handle_500(Some("Custom error message".to_string()))

Returns:

{
  "status": 500,
  "error": "INTERNAL_SERVER_ERROR",
  "message": "An internal server error occurred. Please try again later.",
  "path": null
}

Panic Catcher Middleware

Catches panics and converts them to proper 500 responses:

.layer(from_fn(catch_panic_middleware))

API Endpoints

Health Check

GET /health

Response (200 OK):

{
  "status": "ok"
}

Get Todo

GET /todos/{id}

Response (200 OK):

{
  "id": 42,
  "task": "Example Task",
  "completed": false
}

Response (404 Not Found):

{
  "status": 404,
  "error": "NOT_FOUND",
  "message": "Resource not found: Todo with id 999 not found"
}

Create Todo

POST /todos
Content-Type: application/json

{
  "task": "Buy groceries"
}

Response (201 Created):

{
  "id": 1,
  "task": "Buy groceries",
  "completed": false
}

Response (400 Bad Request - Validation Error):

{
  "status": 400,
  "error": "VALIDATION_ERROR",
  "message": "Validation error: Task cannot be empty"
}

Swagger UI

GET /swagger-ui/

Interactive API documentation.

OpenAPI Specification

GET /api-docs/openapi.json

OpenAPI 3.0 JSON specification.

Usage Examples

Using AppError in Handlers

use axum::{extract::Path, Json};
use crate::errors::AppError;

async fn get_user(Path(id): Path<u64>) -> Result<Json<User>, AppError> {
    let user = database::find_user(id)
        .await
        .ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
    
    Ok(Json(user))
}

Validation Example

async fn create_user(Json(payload): Json<CreateUser>) -> Result<Json<User>, AppError> {
    if payload.email.is_empty() {
        return Err(AppError::Validation("Email is required".to_string()));
    }
    
    if !payload.email.contains('@') {
        return Err(AppError::Validation("Invalid email format".to_string()));
    }
    
    // Process user creation...
    Ok(Json(user))
}

Database Error Handling

async fn update_user(Path(id): Path<u64>) -> Result<Json<User>, AppError> {
    let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(&pool)
        .await?; // Automatically converts sqlx::Error to AppError
    
    Ok(Json(user))
}

Running the Application

Development

cargo run

The server will start on http://0.0.0.0:3000

With Debug Logging

RUST_LOG=debug cargo run

Production Build

cargo build --release
./target/release/axum-template

Testing

Run All Tests

cargo test

Run Unit Tests Only

cargo test --lib

Run Integration Tests Only

cargo test --test integration_test

Run with Output

cargo test -- --nocapture

Test Coverage

The project includes comprehensive tests:

  • Unit Tests: Located in each module (errors.rs, handles.rs)
  • Integration Tests: Located in tests/integration_test.rs
  • Main Tests: Located in src/main.rs

Test categories:

  • Error type conversions
  • Error response formatting
  • 404 handling for non-existent routes
  • Validation errors
  • Resource not found errors
  • Successful request handling
  • Malformed JSON handling
  • Concurrent requests
  • Middleware functionality

Configuration

Environment Variables

  • RUST_LOG: Set logging level (e.g., debug, info, warn, error)

Timeout Configuration

Default timeout is 30 seconds. Modify in lib.rs:

.layer(TimeoutLayer::with_status_code(
    StatusCode::GATEWAY_TIMEOUT,
    Duration::from_secs(30),
))

Error Handling Best Practices

1. Use Specific Error Types

// Good
return Err(AppError::NotFound("User not found".to_string()));

// Avoid
return Err(AppError::InternalServer("User not found".to_string()));

2. Provide Descriptive Messages

// Good
AppError::Validation(format!("Email '{}' is invalid", email))

// Avoid
AppError::Validation("Invalid".to_string())

3. Use ? Operator for Propagation

async fn handler() -> Result<Json<Data>, AppError> {
    let data = database::fetch().await?; // Auto-converts errors
    Ok(Json(data))
}

4. Log Errors at Appropriate Levels

// Errors are automatically logged by the IntoResponse implementation
// Client errors (4xx) -> WARN
// Server errors (5xx) -> ERROR

Adding New Error Types

To add a new error type:

  1. Add variant to AppError enum in errors.rs:
#[error("Too many requests: {0}")]
TooManyRequests(String),
  1. Add status code mapping:
AppError::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
  1. Add error type string:
AppError::TooManyRequests(_) => "TOO_MANY_REQUESTS",
  1. Use in handlers:
if rate_limit_exceeded {
    return Err(AppError::TooManyRequests("Rate limit exceeded".to_string()));
}

Dependencies

  • axum (0.8.8): Web framework
  • tokio (1.43.0): Async runtime
  • serde (1.0.218): Serialization
  • sqlx (0.8.6): Database access
  • thiserror (2.0.17): Error derivation
  • tracing (0.1.44): Structured logging
  • tower-http (0.6.8): HTTP middleware
  • utoipa (5.4.0): OpenAPI documentation

License

[Add your license here]

Contributing

[Add contribution guidelines here]

Support

For issues and questions, please open an issue on the repository.