| Crates.io | tideway |
| lib.rs | tideway |
| version | 0.7.8 |
| created_at | 2025-12-10 21:53:18.389751+00 |
| updated_at | 2026-01-23 22:39:05.88786+00 |
| description | A batteries-included Rust web framework built on Axum for building SaaS applications quickly |
| homepage | |
| repository | https://github.com/jordcodes/tideway-rs |
| max_upload_size | |
| id | 1978843 |
| size | 2,293,512 |
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.
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.7"
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();
}
Run your app:
cargo run
Visit http://localhost:8000/health to see the built-in health check.
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};
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 configuration
Environment 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 filterCreate 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);
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 UnavailableEnhanced Error Responses: All errors automatically return JSON responses with:
{
"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 configuration
Environment 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:
Tideway includes comprehensive examples demonstrating real-world usage:
examples/saas_app.rs - Full-featured SaaS app with:
cargo run --example saas_app --features database,openapi
examples/custom_database.rs - Implementing a custom DatabasePool:
cargo run --example custom_database --features database
examples/redis_cache.rs - Using Redis for caching:
cargo run --example redis_cache --features cache-redis
examples/sessions_example.rs - Session management examples:
cargo run --example sessions_example --features sessions
examples/auth_flow.rs - Complete auth implementation:
cargo run --example auth_flow
examples/testing_example.rs - Testing patterns:
cargo test --example testing_example
examples/validation_example.rs - Request validation:
cargo run --example validation_example --features validation
examples/dev_mode.rs - Development tools:
cargo run --example dev_mode
examples/production_config.rs - Production setup:
cargo run --example production_config
examples/websocket_chat.rs - Real-time chat with rooms:
cargo run --example websocket_chat --features websocket
examples/websocket_notifications.rs - Real-time notifications:
cargo run --example websocket_notifications --features websocket
Tideway follows a layered architecture:
┌─────────────────────────────────┐
│ HTTP Layer │
│ (Routes, Middleware, Handlers) │
├─────────────────────────────────┤
│ Application Core │
│ (Business Logic, Services) │
├─────────────────────────────────┤
│ Infrastructure │
│ (Database, Cache, External APIs)│
└─────────────────────────────────┘
Key Components:
Trait-Based Components:
All requests automatically include:
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:
use tideway::{SeaOrmPool, DatabasePool};
let pool = SeaOrmPool::from_config(&config).await?;
let pool: Arc<dyn DatabasePool> = Arc::new(pool);
Multiple cache backends supported:
cache-redis featureuse 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:
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:
use axum::{body::Body, http::Request};
use tower::ServiceExt;
#[tokio::test]
async fn test_health_endpoint() {
let app = Router::new().merge(health::health_routes());
let response = app.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
Run tests:
cargo test
Tideway adds minimal overhead compared to raw Axum. Benchmarks are available in the benches/ directory.
Run benchmarks:
cargo bench
See 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!