| .woodpecker | ||
| hurl | ||
| scripts | ||
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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::Error→AppError::InternalServersqlx::Error→AppError::DatabaseorAppError::NotFoundserde_json::Error→AppError::BadRequeststd::io::Error→AppError::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:
- Add variant to
AppErrorenum inerrors.rs:
#[error("Too many requests: {0}")]
TooManyRequests(String),
- Add status code mapping:
AppError::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
- Add error type string:
AppError::TooManyRequests(_) => "TOO_MANY_REQUESTS",
- 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.