Comprehensive Rules for building secure, high-performance web APIs with Rust and the Axum framework.
Stop wrestling with boilerplate code and security concerns. These Cursor Rules turn Axum development into a streamlined, secure-by-default experience that eliminates the context-switching between documentation, security checklists, and error handling patterns.
Security Configuration Hell: You know that feeling when you're 3 hours deep into researching proper cookie security, rate limiting configurations, and password hashing implementations? These rules encode battle-tested security patterns directly into your development flow.
Error Handling Inconsistency: Every Rust web developer has been there - handlers returning different error types, some panicking, others returning bare strings. Your API responses become unpredictable, and debugging becomes a nightmare.
Async Runtime Gotchas: Accidentally blocking the Tokio runtime with std::fs calls, forgetting to spawn CPU-intensive tasks properly, or misunderstanding when to use Arc<Mutex<T>> vs Arc<RwLock<T>>.
This isn't just another configuration file. These rules implement a complete architectural pattern that transforms how you build Axum APIs:
Security-First Architecture: Every handler automatically follows secure patterns - HTTPS-only cookies, Argon2 password hashing, constant-time secret comparisons, and proper CORS policies. No more security afterthoughts.
Unified Error Management: One AppError enum handles everything from validation errors to database failures, with structured JSON responses that your frontend teams will actually appreciate:
// Instead of this chaos:
async fn create_user(Json(user): Json<CreateUser>) -> impl IntoResponse {
// Sometimes returns String, sometimes StatusCode, sometimes panics
}
// You get this consistency:
async fn create_user(Json(user): Json<CreateUser>) -> Result<impl IntoResponse, AppError> {
user.validate()?; // Clean validation
// ... your business logic
Ok((StatusCode::CREATED, Json(response)))
}
Type-Safe Database Operations: SQLx integration that catches SQL errors at compile time, not in production. Connection pooling, migrations, and transaction handling become automatic.
60% Faster Security Implementation: Pre-configured middleware stack eliminates the research phase. Rate limiting, CORS, authentication layers, and secure cookie handling work out of the box.
3x Fewer Runtime Errors: Compile-time query validation with SQLx and comprehensive error typing catch issues before deployment. No more "oops, this SQL query has a typo" moments in production.
50% Less Boilerplate Code: Standardized project structure and error handling patterns mean you write business logic, not infrastructure code.
// Hours of research, security concerns, inconsistent error handling
async fn login(Json(creds): Json<LoginRequest>) -> ??? {
// What do I return here? String? StatusCode? Custom type?
// How do I hash passwords securely?
// What about rate limiting?
// Cookie security settings?
}
async fn login(
State(state): State<AppState>,
Json(creds): Json<LoginRequest>
) -> Result<impl IntoResponse, AppError> {
creds.validate()?; // Built-in validation
let user = authenticate_user(&state.db, &creds).await?;
let session = create_secure_session(user.id).await?;
Ok((
StatusCode::OK,
set_secure_cookie("session", &session),
Json(LoginResponse { user_id: user.id })
))
}
Instead of runtime SQL errors, you get compile-time guarantees:
// This fails at compile time if the query is wrong
let user = sqlx::query_as!(
User,
"SELECT id, email, created_at FROM users WHERE email = $1",
email
)
.fetch_optional(&state.db)
.await?;
Every request automatically gets structured logging with request IDs, timing information, and error context:
// Automatically logged for every request:
// {"level":"info","timestamp":"2024-01-15T10:30:45Z","request_id":"abc123","method":"POST","path":"/users","status":201,"latency_ms":45}
# Create new project with the right structure
cargo new my-api --lib
cd my-api
# Add core dependencies
cargo add axum tokio sqlx serde tracing tower tower-http
cargo add --dev assert_json_diff
Copy this exact folder structure - it's optimized for growth:
src/
├── main.rs // Just runtime bootstrap
├── lib.rs // Public API exports
├── api/
│ ├── mod.rs // Router assembly
│ ├── users.rs // User endpoints
│ └── auth.rs // Auth endpoints
├── db/
│ ├── mod.rs // Database layer
│ └── models.rs // Data models
├── error.rs // Single error type
├── middleware/ // Custom middleware
└── tests/ // Integration tests
This single change eliminates 90% of error handling complexity:
// src/error.rs
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("Validation failed: {0}")]
Validation(#[from] validator::ValidationErrors),
#[error("Resource not found")]
NotFound,
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Unauthorized")]
Unauthorized,
#[error("Internal server error")]
Internal(#[from] anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::Validation(err) => (
StatusCode::BAD_REQUEST,
format!("Validation error: {}", err)
),
AppError::NotFound => (
StatusCode::NOT_FOUND,
"Resource not found".to_string()
),
AppError::Database(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Database error occurred".to_string()
),
AppError::Unauthorized => (
StatusCode::UNAUTHORIZED,
"Unauthorized".to_string()
),
AppError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string()
),
};
let body = Json(json!({
"error": {
"message": error_message
}
}));
(status, body).into_response()
}
}
This configuration gives you production-ready security:
let app = Router::new()
.route("/api/v1/users", post(create_user))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::new()
.allow_origin("https://yourdomain.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST])
.allow_headers([AUTHORIZATION, CONTENT_TYPE]))
.layer(RequestBodyLimitLayer::new(1_048_576)) // 1MB limit
.layer(RateLimitLayer::new(100, Duration::from_secs(60)))
);
Day 1: Authentication endpoints that work securely without referring to documentation. Rate limiting that actually protects your API.
Week 1: Error responses that your frontend team doesn't complain about. Database queries that fail at compile time instead of in production.
Month 1: New team members who can contribute to API endpoints immediately because the patterns are consistent and self-documenting.
Quarter 1: Security audits that find fewer issues because security is built into your development workflow, not bolted on afterward.
This isn't about replacing your Rust knowledge - it's about encoding the patterns you wish you'd known from day one directly into your development workflow. Every handler you write follows security best practices automatically. Every error gets handled consistently. Every database query gets validated at compile time.
The result? You spend your time solving business problems, not fighting with infrastructure concerns that should have been solved once and reused everywhere.
You are an expert in Rust 2021, Axum, Tokio, Tower, SQLx, Serde, ThisError, Tracing, Docker, Shuttle, Railway.
Key Principles
- Memory- & type-safety first: rely on the compiler, avoid `unsafe` unless absolutely necessary.
- Explicit over implicit: macro-free routing, clearly typed request/response objects.
- Functional, composable handlers; avoid global mutable state.
- Fail fast: validate early, return rich errors, keep the happy path last.
- Async-first using Tokio; never block the runtime (e.g. avoid std::fs, use `tokio::fs`).
- Security as a default: HTTPS only, secure cookies, rate-limiting, constant-time comparisons for secrets.
- Consistency: run `rustfmt`, `clippy --all-targets --all-features -D warnings` in CI.
- Observability: structured logging with `tracing`, propagate `request_id` through spans.
Rust Language Rules
- Edition: 2021; enable `#![forbid(unsafe_code)]` at crate root.
- Naming
• snake_case for functions/variables.
• PascalCase for structs/enums/traits.
• SCREAMING_SNAKE_CASE for consts & statics.
- Module layout
• `src/main.rs` contains only `fn main()` + runtime bootstrap.
• `src/lib.rs` exposes public API (`pub use crate::api::router`).
• Break features into folders (`auth/`, `users/`, `health/`).
- Visibility: prefer `pub(crate)` over `pub`; do not expose implementation details.
- Error types
• One top-level `error.rs` defining `#[derive(thiserror::Error)] pub enum AppError` implementing `IntoResponse`.
- Concurrency
• Share immutable data via `Arc<T>`; wrap mutability with `Mutex`/`RwLock` only after proving contention is negligible.
- Testing helpers go under `src/tests/` or `mod tests` with `#[cfg(test)]`.
Error Handling & Validation
- All handlers return `Result<impl IntoResponse, AppError>`.
- Build a single `AppError` enum with variants like `Validation(ValidationErrors)`, `NotFound`, `Db(sqlx::Error)`, `Unauthorized`, `Internal(anyhow::Error)`.
- Implement `IntoResponse` for `AppError` mapping to structured JSON:
`{ "error": { "code": "VALIDATION", "message": "email is invalid" } }`.
- Validate input structs with `validator` crate or manual checks in a dedicated `validate()` method; call `input.validate()?` before processing.
- Use early returns and the `?` operator to keep the happy path at the bottom of the function.
Axum Framework Rules
- Router
• Build with `Router::new()`; chain `.route("/users", post(create_user))`.
• Group versioned APIs: `/api/v1`, `/api/v2`.
• Place fallback `handle_404` last.
- Extractors
• Prefer typed extractors: `Json<CreateUser>` not raw `Bytes`.
• Share application state via `State<AppState>` (wrapped in `Arc`).
- Middleware (Tower layers)
• `TraceLayer::new_for_http()` for structured logs.
• `CorsLayer::permissive()` only in dev; restrict origins in prod.
• `RateLimitLayer::new(100, Duration::from_secs(60))` to mitigate DoS.
• Custom auth layer implementing `tower::Service` to inject `AuthContext` into request extensions.
- Security
• Set `cookie.set_secure(true)` and `cookie.set_same_site(SameSite::Lax)`; always sign & encrypt sensitive cookies.
• Use `argon2` for password hashing with unique 16-byte salt per user.
• Store secrets in env vars, load with `dotenvy` only in dev.
- Database (SQLx)
• Use compile-time checked macros: `sqlx::query_as!`.
• Connection pooling: `PgPoolOptions::new().max_connections(20).connect_lazy(&db_url)`.
• Run migrations at startup via `sqlx::migrate!()`.
- Responses
• Prefer `StatusCode::CREATED` with `Location` header after POST creating resources.
• Pagination responses include `X-Total-Count` header and `next`/`prev` links in body.
Testing
- Unit tests: `#[tokio::test] async fn valid_email_is_saved() { ... }`.
- Integration tests use `axum::Router::into_make_service()` with `tower::ServiceExt::oneshot` to send requests.
- Mock database using `sqlx::Executor::begin()` with a transaction rolled back at the end of each test.
- Use `assert_json_diff::assert_json_eq!` for response comparison.
Performance
- Spawn blocking tasks with `tokio::task::spawn_blocking` for CPU-heavy crypto.
- Enable `hyper` HTTP/2 by default, compress responses using `CompressionLayer`.
- Benchmark critical paths with `cargo bench` + `criterion`.
Observability
- Each request log line contains trace-id, method, path, status, latency.
- Emit metrics via `metrics` crate; expose `/metrics` for Prometheus using `axum_prometheus`.
Deployment
- Provide `Dockerfile`:
```Dockerfile
FROM rust:1.72 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bullseye-slim
COPY --from=builder /app/target/release/app /usr/local/bin/app
CMD ["/usr/local/bin/app"]
```
- Use `ENV RUST_LOG=info` and run with `tokio::main(flavor = "multi_thread", worker_threads = 4)`.
- Health check endpoint `/health` returns `200 OK` JSON `{ "status": "ok" }`.
Documentation
- Annotate handlers with `utoipa`/`openapi` macros to auto-generate OpenAPI 3 spec.
- Keep examples in `docs/` synced with actual code via `cargo doc --open`.
Common Pitfalls & Avoidance
- ❌ Blocking I/O in handlers → ✅ use async equivalents.
- ❌ Returning `String` errors → ✅ return `AppError` for uniform responses.
- ❌ Large JSON payloads unbounded → ✅ use `tower_http::limit::RequestBodyLimitLayer::new(1_048_576)` (1 MB).
- ❌ Panicking in threads → ✅ use `std::panic::set_hook` + `tracing::error!` and return `500`.
File/Folder Template
```
src/
├── main.rs // runtime & router mounting
├── lib.rs // re-exports & tests helpers
├── api/
│ ├── mod.rs // pub fn router()
│ ├── users.rs // /users handlers
│ └── auth.rs
├── db/
│ ├── mod.rs // database layer
│ └── models.rs
├── error.rs // AppError
├── middleware/
└── tests/
```