Web Backends Best Practices
Advocacy rules for maintainable, secure, and fast Rust HTTP services.
How to Use This List
- Review during design reviews and before production deploys.
- Convert repeat failures into Clippy lints, CI checks, or ADRs.
- Revisit after major Axum or Tokio upgrades.
A - Project Structure
- Keep handlers thin; put business logic in services. Test services without HTTP.
- One router module per domain area.
users.rs,billing.rs, not one giant file. - Colocate DTOs with routes or a
schemasmodule. Separate input and output types. - Share domain types via a
corecrate in workspaces. Avoid copying structs across binaries. - Document public API contracts. OpenAPI or README with status code tables.
B - Errors & Validation
- One
AppErrorenum implementingIntoResponse. Stable JSON error shape everywhere. - Validate at the HTTP boundary. Serde +
validatorbefore database calls. - Return 422 for semantic validation failures. Document field-level error codes.
- Log 5xx with request ID; hide internals from clients. No stack traces in JSON.
- Never
unwrapin handlers. Use?and typed errors.
C - State & Data Access
- Store
sqlx::PgPoolinAppStatebehindClone. Pool is internally Arc-like. - Size connection pools from worker count and DB limits. Avoid pool × workers > max_connections.
- Use transactions for multi-step writes. See Transactions page.
- Set statement timeouts on pooled connections. Prevent runaway queries.
- Prefer async drivers end-to-end. No blocking postgres in async handlers.
D - Middleware & Security
- Apply tracing, timeout, and body-limit layers globally. Tune per-route exceptions for SSE.
- Authenticate protected subtrees with route layers. Public health stays unauthenticated.
- Never log tokens, cookies, or passwords. Redact Authorization headers in traces.
- Enable CORS deliberately. Not
permissive()in production without review. - Terminate TLS at the edge or use rustls consistently. No mixed TLS bypass paths.
E - Performance & Operations
- Profile before optimizing framework choice. Measure DB and upstream latency first.
- Set graceful shutdown on
axum::serve. Drain in-flight requests on SIGTERM. - Expose
/healthand/readyseparately. Ready checks DB connectivity. - Use structured tracing with
tracing+ JSON subscriber in prod. Correlate with request IDs. - Load-test with realistic payloads. Include auth and serialization in benchmarks.
FAQs
Axum or Actix?
Default new services to Axum; keep Actix when migration cost exceeds benefit.
Biggest production mistake?
Skipping validation, timeouts, and explicit error contracts at the HTTP edge.
How to test?
tower::ServiceExt::oneshot for handlers; testcontainers for DB integration.
Sync handlers?
Avoid blocking I/O; use spawn_blocking for rare CPU-heavy sections.
OpenAPI required?
Recommended for public APIs; optional for internal services with strong types.
How many layers?
Start with trace + timeout + body limit; add auth and rate limit per route group.
Workspace layout?
api binary + domain + db crates keeps compile times manageable.
Config management?
Env vars for secrets; config or figment crate for typed settings.
Dependency updates?
cargo audit in CI; pin major versions in Cargo.toml.
Observability minimum?
Request logs, error rate, latency histograms, and DB pool metrics.
Related
- Web Backends Basics - Getting started
- Error Handling in Handlers - Error enums
- Middleware & Tower - Layer stack
- Testing Web Services - Test harness
- Databases Best Practices - Data layer rules
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.