Tideway is a batteries-included Rust web framework built on Axum and Tokio. It provides opinionated defaults for building SaaS applications quickly while maintaining the performance and flexibility you expect from Rust.
- Fast & Reliable: Built on Axum and Tokio for maximum performance
- Batteries Included: Pre-configured logging, tracing, error handling, and health checks
- Modular Architecture: Organize your application into reusable route modules
- Trait-Based Extensibility: Swap database, cache, session, and email implementations easily
- Production Middleware: Compression, security headers, timeouts, and Prometheus metrics
- Request Validation: Type-safe validation with custom validators and extractors
- Enhanced Error Handling: Rich error responses with context, IDs, and stack traces
- Background Jobs: In-memory and Redis-backed job queues with retry logic
- Email: SMTP and console mailers with support for Resend, SendGrid, and more
- Developer Experience: Alba-style testing, test fixtures, dev mode debugging tools
- WebSocket Support: Real-time communication with connection management and broadcasting
- Type-Safe: Full Rust type safety with excellent error messages
- Production Ready: Graceful shutdown, request IDs, and structured logging out of the box
- Developer Friendly: Simple, intuitive API with sensible defaults
Feature flags are opt-in unless marked Default.
| Feature | Module | Docs | Example | Notes |
|---|---|---|---|---|
feature-gate-errors |
— | — | — | Optional compile-time errors for missing features |
feature-gate-warnings |
— | — | — | Optional warnings for missing features |
macros |
tideway-macros / openapi |
docs/openapi.md |
examples/api_macro_example.rs |
Default |
database |
database |
docs/database_traits.md |
examples/custom_database.rs |
Default (SeaORM) |
database-sqlx |
database |
docs/database_traits.md |
— | WIP |
openapi |
openapi |
docs/openapi.md |
examples/api_macro_example.rs |
Default |
validation |
validation |
docs/validation.md |
examples/validation_example.rs |
— |
metrics |
metrics |
README.md#built-in-middleware |
tests/metrics_integration_test.rs |
— |
cache |
cache |
docs/caching.md |
examples/redis_cache.rs |
— |
cache-redis |
cache |
docs/caching.md |
examples/redis_cache.rs |
— |
sessions |
session |
docs/sessions.md |
examples/sessions_example.rs |
— |
jobs |
jobs |
docs/background_jobs.md |
examples/background_jobs.rs |
— |
jobs-redis |
jobs |
docs/background_jobs.md |
— | — |
websocket |
websocket |
docs/websockets.md |
examples/websocket_chat.rs |
— |
email |
email |
docs/email.md |
examples/email_example.rs |
— |
auth |
auth |
docs/auth.md |
examples/seaorm_auth.rs |
— |
auth-mfa |
auth::mfa |
docs/auth.md |
examples/seaorm_auth.rs |
— |
auth-breach |
auth::breach |
docs/auth.md |
— | — |
test-auth-bypass |
auth |
docs/auth.md |
tests/auth_integration_test.rs |
Tests only |
billing |
billing |
docs/billing.md |
— | — |
billing-seaorm |
billing |
docs/billing.md |
— | — |
test-billing |
billing |
docs/billing.md |
tests/ |
Tests only |
organizations |
organizations |
— | — | Docs TBD |
organizations-seaorm |
organizations |
— | — | Docs TBD |
organizations-billing |
organizations |
— | — | Docs TBD |
test-organizations |
organizations |
— | tests/ |
Tests only |
admin |
admin |
— | — | Docs TBD |
Add Tideway to your Cargo.toml:
[dependencies]
tideway = "0.7.10"
tokio = { version = "1.48", features = ["full"] }use tideway::{self, App, ConfigBuilder};
#[tokio::main]
async fn main() {
// Initialize logging
tideway::init_tracing();
// Create app with default configuration
let app = App::new();
// Start server
app.serve().await.unwrap();
}If you want to serve Tideway with axum::serve, use the middleware-aware router:
use tideway::App;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let app = App::new();
let router = app.into_router_with_middleware();
let listener = TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, router).await
}Note: into_router() does not apply the full middleware stack. Use
into_router_with_middleware() whenever you serve manually.
Run your app:
cargo runVisit http://localhost:8000/health to see the built-in health check.
Use the CLI to scaffold a minimal Tideway app:
cargo install tideway-cli
tideway new my_app
cd my_app
cargo runOr apply the API preset:
tideway new my_app --preset apiWhen no flags are provided, the CLI will prompt you interactively (similar to Vite).
You can disable prompts with --no-prompt.
If you're using coding agents (Codex, Claude Code, OpenCode), start here:
- Use
tideway new my_appand follow the wizard (fastest path). - Add resources with
tideway resource <name> --wire --db --repo --service --paginate --search. - Run
tideway dev --fix-envto boot with env + migrations.
Agent-friendly flags:
--jsonemits machine-readable JSON lines.--planshows planned file operations without writing.
Project-specific guidance lives in SKILLS.md.
Read the full walkthrough at docs/getting_started.md.
See docs/cli.md for command examples.
Common tideway new flags:
| Flag | Example | Purpose |
|---|---|---|
--preset |
--preset api |
Apply a preset scaffold |
--features |
--features auth,database |
Enable crate features |
--with-config |
--with-config |
Generate config.rs / error.rs |
--with-docker |
--with-docker |
Add docker-compose.yml |
--with-ci |
--with-ci |
Add GitHub Actions workflow |
--with-env |
--with-env |
Generate .env.example |
--no-prompt |
--no-prompt |
Disable interactive prompts |
--summary |
--summary false |
Hide the file summary |
Tideway applications are organized into layers:
src/
├── main.rs # Application entry point
├── lib.rs # Library exports
└── routes/ # Your application routes
└── ...
When using Tideway as a dependency, import from the tideway crate:
use tideway::{App, ConfigBuilder, RouteModule, Result, TidewayError};You can define modules with less boilerplate using the module! macro:
tideway::module!(
UsersModule,
prefix = "/api",
routes = [
(get, "/users", list_users),
(post, "/users", create_user),
]
);You can also group multiple methods for the same path:
tideway::module!(
UsersModule,
prefix = "/api",
routes = [
("/users", get => list_users, post => create_user),
]
);OpenAPI per module (optional):
#[cfg(feature = "openapi")]
mod openapi_docs {
tideway::openapi_doc!(pub(crate) UsersDoc, paths(crate::routes::users::list_users));
}
#[cfg(feature = "openapi")]
let openapi = tideway::openapi_merge_module!(openapi_docs, UsersDoc);Quick guards with ensure!:
use tideway::ensure;
ensure!(user.is_admin, TidewayError::forbidden("Admin access required"));
ensure!(user.id != target_id, "Cannot delete your own account");See docs/error_handling.md for more examples.
Testing helpers (HTTP):
use tideway::testing::get as test_get;
use tideway::{App, RouteModule};
let app = App::new().register_module(UsersModule).into_router();
test_get(app, "/api/users")
.execute()
.await
.assert_ok();See docs/testing.md for more helpers.
Configure your application with environment variables or code:
use tideway::ConfigBuilder;
let config = ConfigBuilder::new()
.with_host("0.0.0.0")
.with_port(3000)
.with_log_level("debug")
.with_max_body_size(50 * 1024 * 1024) // 50MB global limit
.from_env() // Override with TIDEWAY_* env vars
.build()?; // Returns Result<Config> - validates configurationEnvironment Variables:
TIDEWAY_HOST- Server host (default: 0.0.0.0)TIDEWAY_PORT- Server port (default: 8000)TIDEWAY_LOG_LEVEL- Log level (default: info)TIDEWAY_LOG_JSON- Enable JSON logging (default: false)TIDEWAY_MAX_BODY_SIZE- Maximum request body size in bytes (default: 10MB)RUST_LOG- Standard Rust log filter
Create modular, reusable route groups with the RouteModule trait:
use axum::{routing::get, Router};
use tideway::RouteModule;
struct UsersModule;
impl RouteModule for UsersModule {
fn routes(&self) -> Router {
Router::new()
.route("/", get(list_users))
.route("/:id", get(get_user))
}
fn prefix(&self) -> Option<&str> {
Some("/api/users")
}
}
// Register module
let app = App::new()
.register_module(UsersModule);Register multiple modules (and optional ones) more concisely:
let app = tideway::register_modules!(
App::new(),
UsersModule,
AdminModule,
);
let app = tideway::register_modules!(
app,
BillingModule;
optional: optional_module
);If you already have a homogeneous list of modules (same type), you can also use
App::register_modules(modules) with any iterator.
For optional-only modules, you can use the helper macro:
let app = tideway::register_optional_modules!(
App::new(),
optional_module,
);Use TidewayError for consistent error responses:
use tideway::{Result, TidewayError, ErrorContext};
use axum::Json;
async fn get_user(id: u64) -> Result<Json<User>> {
let user = database.find(id)
.ok_or_else(|| {
TidewayError::not_found("User not found")
.with_context(
ErrorContext::new()
.with_error_id(uuid::Uuid::new_v4().to_string())
.with_detail(format!("User ID {} does not exist", id))
)
})?;
Ok(Json(user))
}Error Types:
TidewayError::not_found(msg)- 404 Not FoundTidewayError::bad_request(msg)- 400 Bad RequestTidewayError::unauthorized(msg)- 401 UnauthorizedTidewayError::forbidden(msg)- 403 ForbiddenTidewayError::internal(msg)- 500 Internal Server ErrorTidewayError::service_unavailable(msg)- 503 Service Unavailable
Quick Guards with ensure!:
use tideway::{ensure, Result, TidewayError};
fn require_admin(user: &User) -> Result<()> {
ensure!(user.is_admin, TidewayError::forbidden("Admin access required"));
Ok(())
}fn prevent_self_delete(user: &User, target_id: uuid::Uuid) -> Result<()> {
ensure!(user.id != target_id, "Cannot delete your own account");
Ok(())
}Enhanced Error Responses: All errors automatically return JSON responses with:
- Error message
- Unique error ID for tracking
- Optional details and context
- Field-specific validation errors
- Stack traces (in dev mode)
{
"error": "Bad request: Validation failed",
"error_id": "550e8400-e29b-41d4-a716-446655440000",
"details": "Invalid input data",
"field_errors": {
"email": ["must be a valid email"],
"age": ["must be between 18 and 100"]
}
}Validate request data with type-safe extractors:
use tideway::validation::{ValidatedJson, ValidatedQuery, validate_uuid};
use validator::Validate;
use serde::Deserialize;
#[derive(Deserialize, Validate)]
struct CreateUserRequest {
#[validate(email)]
email: String,
#[validate(custom = "validate_uuid")]
organization_id: String,
#[validate(length(min = 8))]
password: String,
}
async fn create_user(
ValidatedJson(req): ValidatedJson<CreateUserRequest>
) -> tideway::Result<axum::Json<serde_json::Value>> {
// req is guaranteed to be valid
Ok(axum::Json(serde_json::json!({"status": "created"})))
}
#[derive(Deserialize, Validate)]
struct SearchQuery {
#[validate(length(min = 1, max = 100))]
q: String,
#[validate(range(min = 1, max = 100))]
limit: Option<u32>,
}
async fn search(
ValidatedQuery(query): ValidatedQuery<SearchQuery>
) -> tideway::Result<axum::Json<serde_json::Value>> {
// query is guaranteed to be valid
Ok(axum::Json(serde_json::json!({"results": []})))
}Custom Validators:
validate_uuid()- UUID v4 validationvalidate_slug()- Slug format validationvalidate_phone()- Phone number validationvalidate_json_string()- JSON string validationvalidate_duration()- Duration format (30s, 5m, 1h, 2d)
Use ApiResponse for standardized JSON responses:
use tideway::{ApiResponse, PaginatedData, PaginationMeta};
use axum::Json;
async fn list_todos(page: u32) -> Json<ApiResponse<PaginatedData<Todo>>> {
let todos = get_todos_from_db(page);
Json(ApiResponse::paginated(todos.items, PaginationMeta {
page,
per_page: 20,
total: todos.total,
}))
}
async fn create_todo() -> tideway::Result<CreatedResponse<Todo>> {
let todo = create_todo_in_db();
Ok(CreatedResponse::new(todo, "/api/todos/123"))
}Response Formats:
// Success response
{
"success": true,
"data": [...],
"message": "Optional message"
}
// Paginated response
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 100
}
}
// Created response (201)
{
"success": true,
"data": {...},
"location": "/api/todos/123"
}The built-in /health endpoint is automatically available. Customize health checks:
use tideway::health::{HealthCheck, ComponentHealth, HealthStatus};
use std::pin::Pin;
struct DatabaseHealthCheck;
impl HealthCheck for DatabaseHealthCheck {
fn name(&self) -> &str {
"database"
}
fn check(&self) -> Pin<Box<dyn Future<Output = ComponentHealth> + Send + '_>> {
Box::pin(async {
// Check database connection
let is_healthy = check_db_connection().await;
ComponentHealth {
name: "database".to_string(),
status: if is_healthy {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
},
message: Some("Database connection status".to_string()),
}
})
}
}Tideway provides Alba-style testing utilities for easy HTTP endpoint testing:
use tideway::testing::{get, post, TestDb};
use tideway::testing::fake;
#[tokio::test]
async fn test_create_user() {
let app = create_app();
let response = post(app, "/api/users")
.with_json(&serde_json::json!({
"email": fake::email(),
"name": fake::name(),
}))
.execute()
.await
.assert_status(201)
.assert_json_path("data.email", fake::email());
}
#[tokio::test]
async fn test_with_database() {
let db = TestDb::new("sqlite::memory:").await.unwrap();
db.seed("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)").await.unwrap();
db.with_transaction_rollback(|tx| async move {
// Test code - transaction will be rolled back
// Database state is isolated between tests
}).await.unwrap();
}Enable development mode for enhanced debugging:
use tideway::{ConfigBuilder, DevConfigBuilder};
let config = ConfigBuilder::new()
.with_dev_config(
DevConfigBuilder::new()
.enabled(true)
.with_stack_traces(true)
.with_request_dumper(true)
.build()
)
.build()?; // Returns Result<Config> - validates configurationEnvironment Variables:
TIDEWAY_DEV_MODE- Enable dev mode (default: false)TIDEWAY_DEV_STACK_TRACES- Include stack traces (default: false)TIDEWAY_DEV_DUMP_REQUESTS- Enable request dumper (default: false)TIDEWAY_DEV_DUMP_PATH- Path pattern to dump (default: all)
Structured logging is enabled by default:
#[tokio::main]
async fn main() {
// Initialize with defaults
tideway::init_tracing();
// Or with custom config
let config = ConfigBuilder::new().build();
tideway::init_tracing_with_config(&config);
tracing::info!("Application started");
tracing::debug!(user_id = 123, "Processing request");
}All HTTP requests are automatically logged with:
- Request ID (x-request-id header)
- Method and URI
- Response status
- Response time in milliseconds
Tideway includes comprehensive examples demonstrating real-world usage:
examples/saas_app.rs - Full-featured SaaS app with:
- Database integration
- JWT authentication
- Rate limiting
- CORS configuration
- OpenAPI documentation
- Health checks
cargo run --example saas_app --features database,openapiexamples/custom_database.rs - Implementing a custom DatabasePool:
cargo run --example custom_database --features databaseexamples/redis_cache.rs - Using Redis for caching:
cargo run --example redis_cache --features cache-redisexamples/sessions_example.rs - Session management examples:
cargo run --example sessions_example --features sessionsexamples/auth_flow.rs - Complete auth implementation:
- User registration
- Login with JWT
- Protected routes
- Public endpoints
cargo run --example auth_flowexamples/testing_example.rs - Testing patterns:
- Alba-style HTTP testing
- Database testing with TestDb
- Error case testing
cargo test --example testing_exampleexamples/validation_example.rs - Request validation:
- ValidatedJson extractor
- ValidatedQuery extractor
- Custom validators
- Field-level error handling
cargo run --example validation_example --features validationexamples/dev_mode.rs - Development tools:
- Enhanced error responses
- Request/response dumper
- Stack trace debugging
cargo run --example dev_modeexamples/production_config.rs - Production setup:
- Environment-based config
- Logging setup
- Graceful shutdown
- Health monitoring
cargo run --example production_configexamples/websocket_chat.rs - Real-time chat with rooms:
- WebSocket connection handling
- Room management
- Broadcasting messages
- User join/leave notifications
cargo run --example websocket_chat --features websocketexamples/websocket_notifications.rs - Real-time notifications:
- Server-to-client push notifications
- User-specific channels
- Integration with background jobs
cargo run --example websocket_notifications --features websocketTideway follows a layered architecture:
┌─────────────────────────────────┐
│ HTTP Layer │
│ (Routes, Middleware, Handlers) │
├─────────────────────────────────┤
│ Application Core │
│ (Business Logic, Services) │
├─────────────────────────────────┤
│ Infrastructure │
│ (Database, Cache, External APIs)│
└─────────────────────────────────┘
Key Components:
- App: Main application structure with routing and middleware
- AppContext: Dependency injection container for shared state (database, cache, sessions)
- RouteModule: Trait for modular route organization
- Config: Environment-aware configuration
- TidewayError: Unified error handling
- ApiResponse: Standardized JSON responses
Trait-Based Components:
- DatabasePool: Abstract database connection pooling (SeaORM, SQLx support)
- Cache: Key-value caching abstraction (in-memory, Redis)
- SessionStore: Session management abstraction (in-memory, cookie-based)
- JobQueue: Background job processing (in-memory, Redis)
- ConnectionManager: WebSocket connection management (rooms, broadcasting)
All requests automatically include:
- Request ID: Unique UUID for request tracking
- Body Size Limit: Global default limit (10MB) to prevent DoS attacks
- Tracing: Structured logging with request/response details
- Error Handling: Automatic error to JSON response conversion
- CORS: Configurable CORS support (disabled by default for security)
- Rate Limiting: Per-IP and global rate limiting (health endpoints excluded)
- Compression: Gzip/Brotli response compression
- Security Headers: HSTS, CSP, X-Frame-Options, and more
- Timeout: Configurable request timeouts
- Request Logging: Structured request/response logging
- Metrics: Prometheus metrics collection (optional, uses route templates when available)
Tideway provides AppContext for dependency injection:
use tideway::{AppContext, SeaOrmPool, InMemoryCache, InMemorySessionStore};
use std::sync::Arc;
let db_pool = Arc::new(SeaOrmPool::from_config(&db_config).await?);
let cache = Arc::new(InMemoryCache::new(10000));
let sessions = Arc::new(InMemorySessionStore::new(Duration::from_secs(3600)));
let context = AppContext::builder()
.with_database(db_pool)
.with_cache(cache)
.with_sessions(sessions)
.build();Use in your handlers:
use axum::extract::State;
use tideway::AppContext;
async fn my_handler(State(ctx): State<AppContext>) -> Json<Response> {
// Use helper methods for cleaner access
if let Ok(cache) = ctx.cache() {
// Use cache - returns error if not configured
}
// Or use optional access
if let Some(cache) = ctx.cache_opt() {
// Use cache - returns None if not configured
}
Json(Response { /* ... */ })
}Tideway supports multiple database backends through the DatabasePool trait:
- SeaORM (default): Full-featured ORM with migrations
- SQLx (coming soon): Async SQL toolkit
use tideway::{SeaOrmPool, DatabasePool};
let pool = SeaOrmPool::from_config(&config).await?;
let pool: Arc<dyn DatabasePool> = Arc::new(pool);Multiple cache backends supported:
- In-Memory: Fast HashMap-based cache (default)
- Redis: Distributed caching with
cache-redisfeature
use tideway::cache::{InMemoryCache, RedisCache};
use tideway::CacheExt; // Provides get<T>() and set<T>()
let cache: Arc<dyn Cache> = Arc::new(InMemoryCache::new(10000));
// Type-safe operations
cache.set("user:123", &user_data, Some(Duration::from_secs(3600))).await?;
let user: Option<User> = cache.get("user:123").await?;Session management with multiple storage backends:
- In-Memory: For development/testing
- Cookie-Based: Encrypted cookie sessions
use tideway::session::{InMemorySessionStore, CookieSessionStore};
use tideway::{SessionStore, SessionData};
let store: Arc<dyn SessionStore> = Arc::new(
InMemorySessionStore::new(Duration::from_secs(3600))
);
let mut session = SessionData::new(Duration::from_secs(3600));
session.set("user_id".to_string(), "123".to_string());
store.save("session-id", session).await?;See docs/database_traits.md, docs/caching.md, and docs/sessions.md for detailed documentation.
Tideway applications are easy to test, and include Alba-style helpers:
use tideway::testing::{get, post};
use tideway::testing::fake;
#[tokio::test]
async fn test_create_user() {
let app = create_app();
post(app, "/api/users")
.with_json(&serde_json::json!({
"email": fake::email(),
"name": fake::name(),
}))
.execute()
.await
.assert_status(201);
}See docs/testing.md and examples/testing_example.rs for more patterns.
Run tests:
cargo test- Rate limiting middleware
- CORS configuration
- OpenAPI/Swagger generation
- Request validation support
- Compression middleware
- Security headers middleware
- Request/response logging
- Timeout middleware
- Prometheus metrics
- Global request body size limit (DoS protection)
- Trait-based database abstraction (SeaORM)
- Trait-based caching (in-memory, Redis)
- Trait-based session management (in-memory, cookies)
- Dependency injection with AppContext
- Custom validators (UUID, slug, phone, JSON, duration)
- ValidatedQuery and ValidatedForm extractors
- Enhanced error handling (context, IDs, stack traces)
- Alba-style testing utilities
- Test fixtures and fake data helpers
- Database testing improvements (seed, reset, transactions)
- Development mode (enhanced errors, request dumper)
- Response helpers (paginated, created, no_content)
- WebSocket support (connection management, rooms, broadcasting)
- SQLx database backend implementation
- CLI tool for scaffolding
- Deployment guides
- Additional cache backends (Memcached)
- Additional session backends (database-backed)
Tideway adds minimal overhead compared to raw Axum. Benchmarks are available in the benches/ directory.
Run benchmarks:
cargo benchSee benches/README.md for detailed performance metrics.
Contributions are welcome! This is currently in early development.
MIT
Built with:
Start building your SaaS with Tideway today!