Introduction

This book is a work in progress. If you can't find the information you need here, check the doc.rs documentation or open a GitHub discussion.

This book is intended as a guide for how to use Roadster, a "batteries included" web framework for Rust designed to get you moving fast 🏎️. Compared to low-level web frameworks such as Axum or Actix, which only provide the functionality to create an API, Roadster aims to provide all the other functionality needed to create a fully-featured backend or fullstack web app. Roadster is designed to provide sensible defaults for all features while remaining highly configurable, customizable, and pluggable. This allows you to focus on creating your application instead of wiring up all of your dependencies, while still allowing you the flexibility to customize your dependencies if needed.

If you're unsure if Roadster is the best fit for your project, a collection of comparisons to other Rust web frameworks can be found in Web framework comparisons. The full list of Roadster's features can be found in Roadster features.

Prerequisite reading

This book assumes the reader has some knowledge of how to program in Rust. If you are new to Rust, you may find the following helpful to better understand how to use Roadster:

In addition, as asynchronous programming is essential for writing performant web apps, some knowledge of async programming in Rust is assumed. If you are unfamiliar with async Rust, you may find the following helpful:

Web framework comparisons

This chapter compares Roadster to other Rust web frameworks to help you determine if Roadster is the best fit for your project.

If you're considering Roadster, we assume you have already decided that Rust is the best fit for your project. Therefore, we do not provide comparisons to non-Rust web frameworks, such as Rails, Django, or Laravel.

Roadster vs. Loco

Loco and Roadster serve similar purposes -- they both aim to reduce the amount of configuring, wiring, and other boilerplate code required to build a backend or full-stack web app in Rust. There are some notable differences, however, both in mission statement and the list of supported features. This section will give a summary of both the similarities and differences between Loco and Roadster.

Feature breakdown

Below is a detailed breakdown of the features included in Roadster and Loco. Note that because both frameworks are based on Axum and Tokio, there's not a lot technically preventing either framework from implementing features they're missing compared to the other. Features that Roadster would like to add in the near future are marked with '*'. Other missing features are not planned but we'd be open to adding if there was enough interest in them.

Last updated in Feb 2025.

FeatureRoadsterLoco
Separate cargo CLI to help with generating code and other tasksβŒβœ…
Custom CLI commandsβœ…βœ…
HTTP APIs via Axumβœ…βœ…
 ↳ Default "ping" and "health" HTTP routesβœ…βœ…
  ↳ Default routes can be disabled via configβœ…βŒ
 ↳ Default middleware configured with sensible defaultsβœ…βœ…
  ↳ Middleware can be customized via config filesβœ…βœ…
  ↳ Middleware execution order can be customized via config filesβœ…βŒ
OpenAPI supportβœ…βœ…
 ↳ built-in via Aideβœ…βŒ
 ↳ 3rd party integration, e.g. Utoipaβœ…βœ…
 ↳ OpenAPI docs explorer http route provided by defaultβœ…βŒ
GRPC API with tonicβœ…βŒ
Channels (websockets and/or http long-polling)βŒβœ…
Support for arbitrary long-running servicesβœ…βŒ
Health checksβœ…βœ…
 ↳ Run in "health" API routeβœ…βœ…
 ↳ Run on app startupβœ…βŒ
 ↳ Run via CLIβœ…βŒ
 ↳ Consumer can provide custom checksβœ…βŒ
Custom app context / Axum state using Axum's FromRefβœ…βŒ
SQL DBβœ…βœ…
 ↳ via Dieselβœ…βŒ
 ↳ via SeaORMβœ…βœ…
  ↳ SeaORM migrations for common DB schemasβœ…
(in lib)
βœ…
(in starters)
Sample JWT Axum extractorβœ…βœ…
 ↳ Multiple JWT standards supportedβœ…βŒ
Emailβœ…βœ…
 ↳ via SMTPβœ…βœ…
 ↳ via Sendgrid's Mail Send APIβœ…βŒ
Storage abstraction❌*βœ…
Cache abstraction❌*βœ…
Background jobsβœ…βœ…
 ↳ via Sidekiqβœ…βœ…
 ↳ via PostgresβŒβœ…
 ↳ via in-process threading with TokioβŒβœ…
Periodic jobsβœ…βœ…
 ↳ via Sidekiqβœ…βœ…
 ↳ via custom schedulerβŒβœ…
Configuration via config filesβœ…βœ…
 ↳ Tomlβœ…βŒ
 ↳ Yamlβœ…βœ…
Config files can be split into multiple filesβœ…βŒ
Config values can be overridden via env varsβœ…βœ…
Tracing via the tracing crateβœ…βœ…
 ↳ Built-in support for trace/metric exporting via OpenTelemetryβœ…βŒ
insta snapshot utilitiesβœ…βœ…
Data seeding and cleanup hooks for tests❌*βœ…
(⚠️ makes tests non-parallelizable)
Mock DB support for testsβœ…βŒ
 ↳ via Temporary Test DBsβœ…βœ…
 ↳ via SeaORM's MockDatabaseβœ…βŒ
 ↳ via TestContainersβœ…βŒ
Allows following any design patternβœ…βŒ
(MVC only)
Lifecycle hooksβœ…βœ…
 ↳ Customizable shutdown signalβœ…βŒ
HTML renderingβœ…βœ…
 ↳ Built-inβŒβœ…
 ↳ via 3rd party integration, e.g. Leptosβœ…βš οΈ (Partial support)
Deployment config generationβŒβœ…
Starter templates❌*βœ…

Roadster vs. Axum

Roadster actually uses Axum to provide an HTTP server, so anything you can do with plain Axum you can do with Roadster. However, using Roadster has some benefits compared to configuring Axum yourself:

  • Roadster registers a collection of common middleware with sensible default configurations. The configurations can also be customized easily via config files. See Axum middleware for more information.
  • Roadster creates an AppContext to use as the Axum State that contains all the dependency objects created by Roadster, such as the DB connection, app config, etc. This can also be extended using Axum's FromRef if you need to provide additional state to your Axum routes. See Axum state for more information.
  • Roadster supports registering API routes using Aide to enable auto-generating an OpenAPI schema and playground. See OpenAPI with Aide for more information.
  • Roadster auto-generates a unique request ID for each request, if one wasn't provided in the request
  • Roadster configures the Tracing crate and enables instrumentation for requests. See Tracing for more information.

Roadster vs. Actix

As mentioned in Roadster vs. Axum, Roadster uses Axum to provide an HTTP server, so it's probably more relevant to compare Actix and Axum. A quick internet search can provide various comparisons, some are included below:

Roadster vs. Leptos

Leptos is a reactive UI web framework for Rust. However, it has a concept of "server functions", an abstraction for calling server-side logic from either the frontend or the backend. Leptos leaves it up to the consumer to set up the backend using Axum or Actix. That's where Roaster comes in -- Roadster takes care of configuring all the backend resources you need using Axum as the HTTP router. So, Roadster and Leptos can be used together to easily build your full-stack web application fully in Rust.

For an example of how to use Leptos with Roadster, see our Leptos examples:

Getting started

For now, see our main README.md which has some instructions for how to get started with Roadster.

See also, our examples:

  • full - Demo of all features
  • app-builder - Demo of the builder-style API to configure the app with Roadster
  • diesel - Demo of using Diesel as the DB ORM instead of SeaORM (the default).
  • leptos-ssr - Demo of using Leptos with Roadster

Roadster features

⬅️ See the side drawer for the list of Roadster's features discussed in this chapter.

Configuration

Roadster provides sensible defaults, but is highly customizable via configuration files and environment variables. Virtually all behavior of Roadster can be configured; to see the available configuration keys, see the AppConfig struct.

To see the full app config that's loaded for your app, use the print-config CLI command. This will print the app config after all the config sources (files, env vars, cli args) have been loaded and merged.

cargo run -- roadster print-config -f toml

Configuration files

The primary way to customize the app's settings is via configuration files. Roadster supports Toml config files by default, and supports Yaml config files with the config-yml feature flag. By default, Roadster looks for config files in the config directory of your project (at the same level as your Cargo.toml). However, if you have the cli feature flag enabled, you can specify a different config directory using the --config-dir CLI parameter.

You can provide a default configuration and override the defaults for each environment stage. Config files can either be in a file named default or the environment name, or in a directory matching the environment name. Example config file structure:

my-app/
└── config/
    β”œβ”€β”€ default.toml
    β”œβ”€β”€ development.toml
    β”œβ”€β”€ test.toml
    β”œβ”€β”€ production.toml
    β”œβ”€β”€ default/
    β”‚   └── db.toml
    β”œβ”€β”€ development/
    β”‚   └── db.toml
    β”œβ”€β”€ test/
    β”‚   └── db.toml
    └── production/
        └── db.toml

If there are multiple files in an environment's directory, they are loaded into the config in lexicographical order, and the last file loaded takes precedence if there are duplicate fields in the files. For example, if an environment directory contains the files a.toml and b.toml, a.toml is loaded first and b.toml is loaded second, and any duplicate fields in b.toml will override the values from a.toml.

Environment variables

You can set environment variables to customize fields. This is useful to provide values for sensitive fields such as DB passwords. Note that setting passwords as env vars does come with some amount of security risk as they are readable by anyone who has access to your server, but they're better than checking your sensitive values into git or other source control.

Env vars can either be set on the command line, or in a .env file. Environment variables should be prefixed with ROADSTER__ named according to the AppConfig structure, where each level of the structure is separated by a double underscore (__). For example, to override the AppConfig#environment, you would use an environment variable named ROADSTER__ENVIRONMENT, and to override AppConfig#app#shutdown_on_error, you would use ROADSTER__APP__SHUTDOWN_ON_ERROR. E.g.:

export ROADSTER__ENVIRONMENT=dev
export ROADSTER__APP__SHUTDOWN_ON_ERROR=true

Custom Sources

You can also provide one or more Sources to add to the configuration. This is primarilty intended to allow overriding specific app config fields for tests, but it can also be used to provide other custom config sources outside of tests.

type App = RoadsterApp<AppContext>;

async fn prepare_app(app: App) -> RoadsterResult<PreparedApp<App, AppContext>> {
    /*
    Config fields can be set using the name of the field, where each level in the config
    is separated by a `.`

    For example, `service.sidekiq.redis.uri` overrides the `AppConfig#service#sidekiq#redis#uri` field.
    See: <https://docs.rs/roadster/latest/roadster/config/service/worker/sidekiq/struct.Redis.html#structfield.uri>

    Note: Take care to not hard-code any sensitive values when providing a custom config source.
    However, it may be okay to hard-code a generic local connection URI (as we're doing here) if
    it's only used for testing (the primary intended purpose of allowing custom `Source`s).
     */
    let options = PrepareOptions::builder()
        .add_config_source(
            ConfigOverrideSource::builder()
                .name("service.sidekiq.redis.uri")
                .value("redis://localhost:6379")
                .build(),
        )
        .build();
    let app = prepare(app, options).await?;
    Ok(app)
}

Custom Async sources

You can also provide one or more AsyncSources to add to the configuration. This is useful to load configuration fields (particularly sensitive ones) from an external service, such as AWS or GCS secrets manager services.

AsyncSources are loaded into the configuration after all the other sources, so they have the highest precedence (they will override any duplicate fields from other sources).

#[derive(Debug)]
pub struct ExampleAsyncSource;

#[async_trait]
impl AsyncSource for ExampleAsyncSource {
    async fn collect(&self) -> Result<config::Map<String, Value>, ConfigError> {
        let mut config = config::Map::new();

        /*
        Config fields can be set using the name of the field, where each level in the config
        is separated by a `.`

        For example, `service.sidekiq.redis.uri` overrides the `AppConfig#service#sidekiq#redis#uri` field.
        See: <https://docs.rs/roadster/latest/roadster/config/service/worker/sidekiq/struct.Redis.html#structfield.uri>

        Note: a hard-coded value is used here for demonstration purposes only. In a real application,
        an `AsyncSource` is intended to fetch the value from an external service, such as AWS or GCS
        secrets manager services.
         */
        config.insert(
            "service.sidekiq.redis.uri".into(),
            "redis://localhost:6379".into(),
        );

        Ok(config)
    }
}

type App = RoadsterApp<AppContext>;

fn build_app() -> App {
    let builder = RoadsterApp::builder();

    let builder = builder.add_async_config_source(ExampleAsyncSource);

    let builder = builder.state_provider(|context| Ok(context));
    builder.build()
}

Config mechanism precedence

  • default.toml (lowest)
  • default/<filename>.toml
  • <env>.toml
  • <env>/<filename>.toml
  • Environment variables
  • Sources
  • AsyncSources (highest -- overrides lower precedence values)

If the config-yml feature is enabled, files with extensions .yml and .yaml. The precedence of all supported file extensions is the following:

  • .yml (lowest)
  • .yaml
  • .toml (highest -- overrides lower precedence values)

Environment names

Roadster provides some pre-defined environment names in the Environment enum.

  • development (alias: dev)
  • test
  • production (alias: prod)

In addition, apps can define a custom environment name, which is mapped to the Environment::Custom enum variant. This is useful for special app-specific environments, such as additional pre-prod or canary environments.

App context

The AppContext is the core container for shared state in Roadster. It can be used as the Axum Router state directly, or you can define your own state type that can be used by both Roadster and Axum by implementing FromRef.

use axum::extract::FromRef;
use roadster::app::context::AppContext;

#[derive(Clone, FromRef)]
pub struct CustomState {
    pub context: AppContext,
    pub custom_field: String,
}

Provide and ProvideRef traits

The Provide and ProvideRef traits allow getting an instance of T from the implementing type. AppContext implements this for various types it contains. This allows a method to specify the type it requires, then the caller of the method can determine how to provide the type. This is a similar concept to dependency injection (DI) in frameworks like Java Spring, though this is far from a full DI system.

This is useful, for example, to allow mocking the DB connection in tests. Your DB operation method would declare a parameter of type ProvideRef<DataBaseConnection>, then your application code would provide the AppContext to the method, and your tests could provide a mocked ProvideRef instance that returns a mock DB connection. Note that mocking the DB comes with its own set of trade-offs, for example, it may not exactly match the behavior of an actual DB that's used in production. Consider testing against an actual DB instead of mocking, e.g., by using test containers.

Mocked implementations of the traits are provided if the testing-mocks feature is enabled.

pub async fn check_db_health(context: AppContext) -> RoadsterResult<()> {
    // `ping_db` can be called with `AppContext` in order to use the actual `DatabaseConnection`
    // in production
    ping_db(context).await
}

/// Example app method that takes a [`ProvideRef`] in order to allow using a mocked
/// [`DatabaseConnection`] in tests.
async fn ping_db(db: impl ProvideRef<DatabaseConnection>) -> RoadsterResult<()> {
    db.provide().ping().await?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use roadster::app::context::MockProvideRef;
    use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};

    #[tokio::test]
    async fn db_ping() {
        let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
        let mut db_provider = MockProvideRef::<DatabaseConnection>::new();
        db_provider.expect_provide().return_const(db);

        super::ping_db(db_provider).await.unwrap();
    }
}

See also:

Weak reference

In some cases, it can be useful to have a weak reference to the AppContext state in order to prevent reference cycles for things that are included in the AppContext but also need a reference to the AppContext. For example, the AppContext keeps a reference to the HealthChecks, and most HealthChecks need to use the AppContext.

To get a weak reference to the AppContext's state, use AppContext#downgrade to get a new instance of AppContextWeak.


pub struct ExampleHealthCheck {
    // Prevent reference cycle because the `ExampleHealthCheck` is also stored in the `AppContext`
    context: AppContextWeak,
}

#[async_trait]
impl HealthCheck for ExampleHealthCheck {
    fn name(&self) -> String {
        "example".to_string()
    }

    fn enabled(&self) -> bool {
        true
    }

    async fn check(&self) -> RoadsterResult<CheckResponse> {
        // Upgrade the `AppContext` in order to use it
        let _context = self
            .context
            .upgrade()
            .ok_or_else(|| anyhow!("Could not upgrade AppContextWeak"))?;

        Ok(CheckResponse::builder()
            .status(Status::Ok)
            .latency(Duration::from_secs(0))
            .build())
    }
}

pub fn build_app() -> App {
    RoadsterApp::builder()
        .state_provider(|context| {
            Ok(CustomState {
                context,
                custom_field: "Custom Field".to_string(),
            })
        })
        .add_health_check_provider(|registry, state| {
            // Downgrade the context before providing it to the `ExampleHealthCheck`
            let context = AppContext::from_ref(state).downgrade();
            registry.register(ExampleHealthCheck { context })
        })
        .build()
}

See also

Database

Roadster provides built-in support for both SeaORM and Diesel.

SeaORM

When the db-sea-orm feature is enabled, Roadster provides support for various SQL databases via SeaORM, an ORM built on top of sqlx. See the SeaORM docs for more details.

Migrator

To run your SeaORM migrations with Roadster, provide your MigratorTrait type to Roadster. This is done by providing the migrator via the RoadsterAppBuilder#sea_orm_migrator method


fn build_app() -> App {
    RoadsterApp::builder()
        .state_provider(|context| Ok(context))
        .sea_orm_migrator(Migrator)
        .build()
}

or the App#migrators impl

pub struct MyApp;

#[async_trait]
impl App<AppContext> for MyApp {
    type Cli = crate::cli::Cli;

    async fn provide_state(&self, context: AppContext) -> RoadsterResult<AppContext> {
        Ok(context)
    }

    fn migrators(
        &self,
        _state: &AppContext,
    ) -> RoadsterResult<Vec<Box<dyn roadster::db::migration::Migrator<AppContext>>>> {
        Ok(vec![Box::new(SeaOrmMigrator::new(Migrator))])
    }
}

Migration utilities

Roadster provides some utilities for defining common column types with SeaORM. See the migration module docs for the list of utilities.

Diesel

When one of the db-diesel-* features is enabled, Roadster provides support for various SQL databases via Diesel. See the Diesel docs for more details.

Diesel connection pool

Because of Diesel's strong typing, Roadster has different features for each DB backend, each of which enables the respective DB connection pool. In addition to the non-async connections provided by diesel, async versions of the Postgres and Mysql connections are supported via diesel-async.

FeatureConnection type
db-diesel-postgres-poolNon-async Postgres
db-diesel-mysql-poolNon-async Mysql
db-diesel-sqlite-poolNon-async Sqlite
db-diesel-postgres-pool-asyncAsync Postgres
db-diesel-mysql-pool-asyncAsync Mysql

Migrator

To run your Diesel migrations with Roadster, provide your migrations to Roadster. This is done by setting providing the migrator in the RoadsterAppBuilder#diesel_migrator method

const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");

fn build_app() -> App {
    RoadsterApp::builder()
        .state_provider(|context| Ok(context))
        .diesel_migrator::<roadster::db::DieselPgConn>(MIGRATIONS)
        .build()
}

or the App#migrators impl

pub struct MyApp;

const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");

#[async_trait]
impl App<AppContext> for MyApp {
    type Cli = crate::cli::Cli;

    async fn provide_state(&self, context: AppContext) -> RoadsterResult<AppContext> {
        Ok(context)
    }

    fn migrators(
        &self,
        _state: &AppContext,
    ) -> RoadsterResult<Vec<Box<dyn roadster::db::migration::Migrator<AppContext>>>> {
        Ok(vec![Box::new(
            DieselMigrator::<roadster::db::DieselPgConn>::new(MIGRATIONS),
        )])
    }
}

Running migrations

Automatically

Roadster can automatically run your migrations when your app is starting. This behavior is configured by the database.auto-migrate config field.

[database]
auto-migrate = true # change to `false` to disable

Via the Roadster CLI

You can also manually run migrations via the CLI (when the cli feature is enabled).

cargo run -- roadster migrate up

Note that this does not generate schemas/entities that may be generated by the SeaORM/Diesel CLI utilities. This only runs the migrations on the connected DB.

Via ORM CLI

SeaORM

sea migrate up

See: https://crates.io/crates/sea-orm-cli

Diesel

diesel migration run

See: https://crates.io/crates/diesel_cli

User SQL migrations

See:

Utility SQL migrations

See:

Services

An AppService is a long-running, persistent task that's the primary way to add functionality to your app. Roadster provides some AppServices, such as HttpService, SidekiqWorkerService, and the general FunctionService.

Registering a service

In order to run a service in your app, it needs to be registered with the service registry.

use roadster::app::context::AppContext;
use roadster::service::http::service::HttpService;

type App = RoadsterApp<AppContext>;

fn build_app() -> App {
    RoadsterApp::builder()
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        .add_service_provider(move |registry, state| {
            Box::pin(async move {
                registry
                    .register_builder(HttpService::builder(Some("/api"), state))
                    .await?;
                Ok(())
            })
        })
        .build()
}

HTTP Service with Axum

The HttpService provides support for serving an HTTP API using axum. The HttpService automatically applies all the configured middleware and initializers automatically, so all that's needed in most cases to serve a production ready API service is to define your routes, provide them to the HttpService, and register the HttpService with the ServiceRegistry.

use roadster::service::http::builder::HttpServiceBuilder;

const BASE: &str = "/api";

/// Set up the [`HttpServiceBuilder`]. This will then be registered with the
/// [`roadster::service::registry::ServiceRegistry`].
pub fn http_service(state: &AppContext) -> HttpServiceBuilder<AppContext> {
    HttpServiceBuilder::new(Some(BASE), state)
        // Multiple routers can be registered and they will all be merged together using the
        // `axum::Router::merge` method.
        .router(Router::new().route(&build_path(BASE, "/example_a"), get(example_a)))
        // Create your routes as an `ApiRouter` in order to include it in the OpenAPI schema.
        .api_router(ApiRouter::new().api_route(
            &build_path(BASE, "/example_b"),
            get_with(example_b::example_b_get, example_b::example_b_get_docs),
        ))
        .api_router(ApiRouter::new().api_route(
            &build_path(BASE, "/example_c"),
            get_with(example_c::example_c_get, example_c::example_c_get_docs),
        ))
}

async fn example_a() -> impl IntoResponse {
    ()
}
example_b module
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExampleBResponse {}

#[instrument(skip_all)]
pub async fn example_b_get(
    State(_state): State<AppContext>,
) -> RoadsterResult<Json<ExampleBResponse>> {
    Ok(Json(ExampleBResponse {}))
}

pub fn example_b_get_docs(op: TransformOperation) -> TransformOperation {
    op.description("Example B API.")
        .tag("Example B")
        .response_with::<200, Json<ExampleBResponse>, _>(|res| res.example(ExampleBResponse {}))
}
example_c module
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExampleCResponse {}

#[instrument(skip_all)]
pub async fn example_c_get(
    State(_state): State<AppContext>,
) -> RoadsterResult<Json<ExampleCResponse>> {
    Ok(Json(ExampleCResponse {}))
}

pub fn example_c_get_docs(op: TransformOperation) -> TransformOperation {
    op.description("Example C API.")
        .tag("Example C")
        .response_with::<200, Json<ExampleCResponse>, _>(|res| res.example(ExampleCResponse {}))
}

OpenAPI Schema

If the open-api feature is enabled, the service also supports generating an OpenAPI schema. The OpenAPI schema can be accessed in various ways.

Via HTTP API

It's served by default at /<base>/_docs/api.json

# First, run your app
cargo run

# In a separate shell or browser, navigate to the API, e.g.
curl localhost:3000/api/_docs/api.json

Via CLI command

It can be generated via a CLI command

cargo run -- roadster open-api -o $HOME/open-api.json

Via the HttpService directly

It can also be generated programmatically using the HttpService directly.

type App = RoadsterApp<AppContext>;

async fn open_api() -> RoadsterResult<()> {
    // Build the app
    let app: App = RoadsterApp::builder()
        .state_provider(|context| Ok(context))
        .add_service_provider(move |registry, state| {
            Box::pin(async move {
                registry
                    .register_builder(crate::http::http_service(state))
                    .await?;
                Ok(())
            })
        })
        .build();

    // Prepare the app
    let prepared = prepare(app, PrepareOptions::builder().build()).await?;

    // Get the `HttpService`
    let http_service = prepared.service_registry.get::<HttpService>()?;

    // Get the OpenAPI schema
    let schema = http_service.open_api_schema(&OpenApiArgs::builder().build())?;

    println!("{schema}");

    Ok(())
}

Axum State

Axum allows providing a "state" struct to the Router. Roadster provides its state (DB connection pool, etc) in the AppContext struct, which can be used either as the Axum state directly. Or, if non-Roadster state is needed for some resource not provided by Roadster, a custom struct can be used as long as it implements FromRef so Roadster can get its AppContext state from Axum.

FromRef for custom state

FromRef can either be derived

#[derive(Clone, FromRef)]
pub struct CustomState {
    pub context: AppContext,
    pub custom_field: String,
}

or implemented manually

#[derive(Clone)]
pub struct CustomState {
    pub context: AppContext,
    pub custom_field: String,
}

impl FromRef<CustomState> for AppContext {
    fn from_ref(input: &CustomState) -> Self {
        input.context.clone()
    }
}

Providing state

The app state needs to be provided to the HttpService when it's created. If the HttpServiceBuilder is used to register the service with the ServiceRegistry#register_builder method, the state will be provided automatically when the ServiceRegistry builds the service.

use roadster::app::context::AppContext;
use roadster::service::http::service::HttpService;

type App = RoadsterApp<AppContext>;

fn build_app() -> App {
    RoadsterApp::builder()
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        .add_service_provider(move |registry, state| {
            Box::pin(async move {
                registry
                    .register_builder(HttpService::builder(Some("/api"), state))
                    .await?;
                Ok(())
            })
        })
        .build()
}

See also

Axum Middleware

Roadster's HTTP service has full support for any Axum (or Tower) middleware, many of which are provided by default. It's also possible to register middleware that Roadster doesn't provide by default, or even create and provide your own custom middleware.

Default middleware

For the most up to date list of middleware provided by Roadster, see the module's docs.rs page

All of the default middleware can be configured via the app's config files. All middleware have at least the following config fields:

  • enable: Whether the middleware is enabled. If not provided, the middleware enablement falls back to the value of the service.http.middleware.default-enable field.
  • priority: The priority in which the middleware will run. Lower values (including negative numbers) run before higher values. The middlewares provided by Roadster have priorities between -10,000 (runs first) and 10,000 (runs later) by default, though these values can be overridden via configs. If the order your middleware runs in doesn't matter, simply set to 0.

Custom middleware

Custom middleware can be provided by implementing the Middleware trait. As a convenience, custom middleware can also be applied using the AnyMiddleware utility. This is useful, for example, for middleware that can be built using Axum's from_fn method.

const BASE: &str = "/api";

/// Set up the [`HttpServiceBuilder`]. This will then be registered with the
/// [`roadster::service::registry::ServiceRegistry`].
pub async fn http_service(state: &AppContext) -> RoadsterResult<HttpServiceBuilder<AppContext>> {
    HttpServiceBuilder::new(Some(BASE), state)
        .api_router(ApiRouter::new().api_route(
            &build_path(BASE, "/example_b"),
            get_with(example_b::example_b_get, example_b::example_b_get_docs),
        ))
        .middleware(
            AnyMiddleware::builder()
                .name("custom-middleware")
                .apply(|router, _state| {
                    let router = router.layer(axum::middleware::from_fn(custom_middleware_fn));
                    Ok(router)
                })
                .build(),
        )
}

async fn custom_middleware_fn(request: Request, next: Next) -> Response {
    info!("Running custom middleware");

    next.run(request).await
}

Initializers

Initializers are similar to Middleware -- they both allow configuring the Axum Router for your app's HTTP service. However, Initializers provide more precise control over when it is applied to the Router. This is useful to apply middleware that requires a fully set up Router in order to work as expected, e.g. Tower's NormalizePathLayer. It's also useful in order to initialize Extensions that you want to attach to the Router -- this is most useful for using external utility crates that expect state to be in an Extension, most state you need in your own application code should probably be in a custom state struct.

Initializer hooks

Initializers have various hooks to allow modifying the Router at a particular point in the process of building it. They are listed below in the order in which they are run:

  1. after_router: Runs after all of the routes have been added to the Router
  2. before_middleware: Runs before any Middleware is added to the Router.
  3. after_middleware: Runs after all Middleware has been added to the Router.
  4. before_serve: Runs right before the HTTP service starts.

Default initializers

Currently, the only Initializer provided by Roadster is the [NormalizePathInitializer]. This initializer applies Tower's NormalizePathLayer after all other Router setup has completed, which is required in order for it to properly normalize paths (e.g., treat paths with and without trailing slashes as the same path).

If more Initializers are added in the future, they can be found in the module's docs.rs page.

All of the default initializers can be configured via the app's config files. All initializers have at least the following config fields:

  • enable: Whether the initializer is enabled. If not provided, the initializer enablement falls back to the value of the service.http.initializer.default-enable field.
  • priority: The priority in which the initializer will run. Lower values (including negative numbers) run before higher values. The middlewares provided by Roadster have priorities between -10,000 (runs first) and 10,000 (runs later) by default, though these values can be overridden via configs. If the order your initializer runs in relative to other initializers doesn't matter, simply set to 0.

Custom initializers

Custom initializers can be provided by implementing the Initializer trait. As a convenience, custom initializers can also be applied using the AnyInitializer utility. This is useful to run an initializer without adding a full struct + trait implementation.

const BASE: &str = "/api";

/// Set up the [`HttpServiceBuilder`]. This will then be registered with the
/// [`roadster::service::registry::ServiceRegistry`].
pub async fn http_service(state: &AppContext) -> RoadsterResult<HttpServiceBuilder<AppContext>> {
    HttpServiceBuilder::new(Some(BASE), state)
        .api_router(ApiRouter::new().api_route(
            &build_path(BASE, "/example_b"),
            get_with(example_b::example_b_get, example_b::example_b_get_docs),
        ))
        .initializer(
            AnyInitializer::builder()
                .name("custom-initializer")
                .apply(|router, _state| {
                    info!("Running custom initializer");
                    Ok(router)
                })
                .build(),
        )
}

Background jobs

In virtually all apps, there exists some work that needs to be done outside of the "critical path" in order to provide a quick and responsive experience to the user. For example, in mobile apps, the only work that (should) happen on the main thread is updating the UI. Everything else, such as reading files from disk and fetching data from the server, happens on a background thread.

In web apps (and API servers), the same principle applies. In general APIs should do the minimal amount of work needed in order to response to the user's (or other service's) API request, and everything else should be moved to some background "process". There are many ways this can be done; for example, AWS SQS, GCP Pub/Sub, Sidekiq, Faktory, to name a few.

Background jobs with Sidekiq

Sidekiq is a popular system for running background and cron jobs in Ruby on Rails apps. Roadster provides built-in support for running background jobs with Sidekiq via the Sidekiq.rs crate, which provides a Rust interface for interacting with a Sidekiq server (e.g., a Redis server).

Below is an example of how to register a worker and enqueue it into the job queue. See the Sidekiq.rs for more details on implementing Workers.

Service configs

Various properties of the Sidekiq worker service can be configured via the app's config files. The most important fields to configure are the following:

  • service.sidekiq.num-workers: The number of Sidekiq workers that can run at the same time.
  • service.sidekiq.queues: The names of the worker queues to handle.
  • service.sidekiq.redis.uri: The URI of the Redis database to use as the Sidekiq server.
[service.sidekiq]
num-workers = 2
queues = ["default"]

[service.sidekiq.redis]
# A hard-coded value can be provided to connect to a local server for local development.
# Production values should be provided via a more secure method, such as an environment var
# or an `AsyncSource` that fetches from an external secrets manager.
uri = "redis://localhost:6379"

See the config struct for the full list of fields available.

Worker configs

In addition to the service-level configs, each worker has various configurable values. Some of these can be provided by implementing the respective methods of the sidekiq.rs Worker trait. However, they can also be provided when the worker is registered with the SidekiqWorkerServiceBuilder.

    service.register_worker_with_config(
        ExampleWorker::new(context),
        AppWorkerConfig::builder()
            .max_retries(3)
            .timeout(true)
            .max_duration(Duration::from_secs(30))
            .build(),
    )?;

Roadster worker

All workers registered with the SidekiqWorkerServiceBuilder are wrapped in our custom RoadsterWorker. This allows us to implement some additional features for workers. Specifically, the ability to set a max duration for workers, after which they will automatically timeout, be reported as an error, and be retried according to the worker's retry config. The default behavior is to timeout after 60 seconds, but this can be extended or disabled at the service level or in each individual worker.

Note: in order for a worker to stop running when the timeout is exceeded, the worker needs to hit an await point. So, it will work great for async IO-bound tasks, but CPU-bound tasks will require manual yields (e.g. with yield_now) in order for the tasks to be automatically timed out.

Example

pub struct ExampleWorker {
    // If the worker needs access to your app's state, it can be added as a field in the worker.
    state: AppContext,
}

impl ExampleWorker {
    pub fn new(state: &AppContext) -> Self {
        Self {
            state: state.clone(),
        }
    }
}

// Implement the `Worker` trait
#[async_trait]
impl Worker<String> for ExampleWorker {
    async fn perform(&self, args: String) -> sidekiq::Result<()> {
        info!("Processing job with args: {args}");
        Ok(())
    }
}

fn build_app() -> RoadsterApp<AppContext> {
    RoadsterApp::builder()
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        // Register the Sidekiq worker service
        .add_service_provider(move |registry, state| {
            Box::pin(async move {
                registry
                    .register_builder(
                        SidekiqWorkerService::builder(state)
                            .await?
                            // Register the `ExampleWorker` with the sidekiq service
                            .register_worker(ExampleWorker::new(state))?
                            // Optionally register the worker with worker-level config overrides
                            .register_worker_with_config(
                                ExampleWorker::new(state),
                                AppWorkerConfig::builder()
                                    .max_retries(3)
                                    .timeout(true)
                                    .max_duration(Duration::from_secs(30))
                                    .build(),
                            )?
                            // Register the `ExampleWorker` to run as a periodic cron job
                            .register_periodic_worker(
                                sidekiq::periodic::builder("* * * * * *")?
                                    .name("Example periodic worker"),
                                ExampleWorker::new(state),
                                "Periodic example args".to_string(),
                            )
                            .await?,
                    )
                    .await?;
                Ok(())
            })
        })
        .build()
}

async fn example_get(State(state): State<AppContext>) -> RoadsterResult<()> {
    // Enqueue the job in your API handler
    ExampleWorker::enqueue(&state, "Example".to_string()).await?;

    Ok(())
}

gRPC service with Tonic

Roadster best supports API services using Axum. However, we do provide a gRPC service via a basic integration with tonic. Support is pretty minimal and you'll need to manage building your gRPC Router yourself. However, once it's built, Roadster can take care of running it for you.

fn build_app() -> RoadsterResult<RoadsterApp<AppContext>> {
    let app = RoadsterApp::builder()
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        // Register the gRPC service with the provided routes
        .add_service(GrpcService::new(routes()?))
        .build();
    Ok(app)
}

/// Build the gRPC [`Router`].
fn routes() -> RoadsterResult<Router> {
    let reflection_service = tonic_reflection::server::Builder::configure()
        .register_encoded_file_descriptor_set(hello_world::FILE_DESCRIPTOR_SET)
        .build_v1()?;

    let router = Server::builder()
        .add_service(reflection_service)
        .add_service(GreeterServer::new(MyGreeter));

    Ok(router)
}

Function service

If you need to run some long-running service in your app and Roadster doesn't provide built-in support for the specific service you need, you can implement AppService directly. This gives you the most control over the service, especially if you implement AppServiceBuilder as well.

If you don't want to implement AppService yourself, you can simply run the service in an async function and pass that function to a FunctionService.

fn build_app() -> RoadsterApp<AppContext> {
    RoadsterApp::builder()
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        // Register the example function-based service
        .add_service(
            FunctionService::builder()
                .name("example-service")
                .function(example_service)
                .build(),
        )
        .build()
}

async fn example_service(
    _state: AppContext,
    _cancel_token: CancellationToken,
) -> RoadsterResult<()> {
    info!("Running example function-based service");
    Ok(())
}

Auth

Auth (Authentication and Authorization) is an important component of virtually any app. There are many ways to go about adding auth to an app, from implementing your own auth, to using a third-party OAuth service such as Auth0 or Clerk, or using something like Supabase Auth that's somewhere in the middle.

Roadster's auth support is somewhat limited at the moment. However, a common component of many auth systems is Json Web Tokens (JWTs). Roadster provides Axum extractors for a couple JWT standards that should help integrating with any auth implementation you choose.

In the future, Roadster may provide a more opinionated solution for auth. For now, see the following chapters for details about the auth features Roadster currently supports.

JWTs

JSON Web Tokens (JWTs) are a common component of many auth systems. Each auth system will have a slightly different set of fields available in the JWT; however, there are a few common fields as well as a few standards that could be used by any implementation, particularly a custom auth system.

JWT extractor

To make it easier to use JWTs with Roadster, we provide an Axum JWT extractor to get the JWT from the Bearer Authorization header. By default the extractor will extract all claims to the IETF standard (plus any custom claims to a map), or a custom claim struct can be provided instead.

IETF claims

/// Example extracting the IETF [`ietf::Claims`] and a map of custom fields for the JWT.
async fn api_with_ietf_claims(jwt: Jwt) -> impl IntoResponse {
    info!(subject=?jwt.claims.subject, user=?jwt.claims.custom.get("userId"), "Handling request");
}

struct CustomClaims {
    user_id: Uuid,
}

/// Example extracting both the IETF [`ietf::Claims`] and the [`CustomClaims`] for the JWT.
async fn api_with_ietf_and_custom_claims(
    jwt: Jwt<ietf::Claims<CustomClaims>>,
) -> impl IntoResponse {
    info!(subject=?jwt.claims.subject, user=%jwt.claims.custom.user_id,
        "Handling request",
    );
}

/// Example using the [`CustomClaims`] as the only claims extracted for the JWT.
async fn api_with_custom_claims(jwt: Jwt<CustomClaims>) -> impl IntoResponse {
    info!(user=%jwt.claims.user_id, "Handling request",);
}

OpenID claims

Along with the IETF claims, Roadster also provides a claims struct to extract OpenID standard claims.

/// Example extracting the OpenID [`Claims`] and a map of custom fields for the JWT.
async fn api_with_openid_claims(jwt: Jwt<openid::Claims>) -> impl IntoResponse {
    info!(subject=?jwt.claims.subject, user=?jwt.claims.custom.get("userId"), "Handling request");
}

struct CustomClaims {
    user_id: Uuid,
}

/// Example extracting both the OpenID [`Claims`] and the [`CustomClaims`] for the JWT.
async fn api_with_openid_and_custom_claims(
    jwt: Jwt<openid::Claims<CustomClaims>>,
) -> impl IntoResponse {
    info!(subject=?jwt.claims.subject, user=%jwt.claims.custom.user_id,
        "Handling request",
    );
}

JwtCsrf extractor

Some apps may want or need to support clients that don't have javascript available. In those cases, the app will typically set an auth cookie so it can be sent automatically by the client on every request. However, THIS MAY MAKE THE APPLICATION VULNERABLE TO CSRF ATTACKS.

Roadster provides a special JwtCsrf extractor that allows extracting a JWT either from the cookies sent by the client or the Bearer Authorization header as normal. The extractor contains a special field that indicates whether it's safe to use or if the server needs to apply some CSRF protections before the token can be safely used. The token is considered safe if it was extracted from the Bearer Authorization header, or if the request is an HTTP verb that does not modify resources (e.g. GET). In any other case, e.g. the JWT is extract from a cookie and the HTTP verb is a POST, the server must apply a CSRF protection mechanism before using the token. If the JwtCsrf extractor is used for a route with a GET verb, take care not to modify resources, otherwise the application will still be vulnerable to CSRF attacks.

See the following for more information and recommendations for how to implement CSRF protection mechanisms:

If the functionality to extract from a cookie is not required, it’s recommended to use the normal Jwt extractor directly.

OpenAPI with Aide

If the open-api feature is enabled, an OpenAPI schema can be built for the app's Axum API by registering API routes using Aide. The schema will then be generated and served at the /api/_docs/api.json route by default, and is also accessible via CLI commands or the HttpService#open_api_schema method.

Register routes

OpenAPI routes are registered with Aide's ApiRouter, which has a similar API to Axum's Router.

const BASE: &str = "/api";

pub fn http_service(state: &AppContext) -> HttpServiceBuilder<AppContext> {
    HttpServiceBuilder::new(Some(BASE), state)
        // Create your routes as an `ApiRouter` in order to include it in the OpenAPI schema.
        .api_router(
            ApiRouter::new()
                // Register a `GET` route on the `ApiRouter`
                .api_route(
                    &build_path(BASE, "/example"),
                    get_with(example_get, example_get_docs),
                ),
        )
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExampleResponse {}

#[instrument(skip_all)]
pub async fn example_get(
    State(_state): State<AppContext>,
) -> RoadsterResult<Json<ExampleResponse>> {
    Ok(Json(ExampleResponse {}))
}

pub fn example_get_docs(op: TransformOperation) -> TransformOperation {
    op.description("Example API.")
        .tag("Example")
        .response_with::<200, Json<ExampleResponse>, _>(|res| res.example(ExampleResponse {}))
}

Get schema via API route

By default, the generated schema will be served at /api/_docs/api.json. This route can be configured via the service.http.default-routes.api-schema.route config field.

# First, run your app
cargo run

# In a separate shell or browser, navigate to the API, e.g.
curl localhost:3000/api/_docs/api.json

Get schema via CLI

The schema can also be generated via a CLI command

cargo run -- roadster open-api -o $HOME/open-api.json

Get schema from the HttpService

The schema can also be generated programmatically using the HttpService directly.

type App = RoadsterApp<AppContext>;

async fn open_api() -> RoadsterResult<()> {
    // Build the app
    let app: App = RoadsterApp::builder()
        .state_provider(|context| Ok(context))
        .add_service_provider(move |registry, state| {
            Box::pin(async move {
                registry
                    .register_builder(crate::http::http_service(state))
                    .await?;
                Ok(())
            })
        })
        .build();

    // Prepare the app
    let prepared = prepare(app, PrepareOptions::builder().build()).await?;

    // Get the `HttpService`
    let http_service = prepared.service_registry.get::<HttpService>()?;

    // Get the OpenAPI schema
    let schema = http_service.open_api_schema(&OpenApiArgs::builder().build())?;

    println!("{schema}");

    Ok(())
}

Email

Sending emails is a major requirement of many web apps. At a minimum, a web app will almost certainly want to send emails related to auth, e.g. account verification and password recovery.

Roadster provides some minimal configuration of email clients, either via plain SMTP with the lettre crate, or via Sendgrid with the sendgrid crate.

In either case, emails should generally be sent in a background process, e.g. via a Sidekiq worker.

In the future, we may provide some utilities to reduce the boilerplate required to send an email, e.g. by providing some EmailWorker trait/struct that provides common functionality for all emails. This is not implemented yet, but it is something we'd like to add in the future.

The following chapters cover the provided SMTP and Sendgrid integrations.

SMTP

Roadster's SMTP support allows sending emails with virtually any email provider. The caveat with using plain SMTP is you may not be able to use the visual email builders provided by some vendors (e.g. Sendgrid or Customer.io). This means that while you're code will be decoupled from any particular vendor, some additional work will be needed to send "pretty" emails that match your app's design style. However, if you just need to send plain text emails, or are willing to do the additional work to create "pretty" emails, SMTP is a great option for sending emails for your app.

Starting a local SMTP service for testing

There are several SMTP servers that can be run locally for testing. This is another benefit of using SMTP instead of Sendgrid -- a local SMTP instance can be used to easily verify the contents of your emails and your sending logic, while Sendgrid only provides minimal dev/testing support and can't be run locally.

Below are a few options for development SMTP services that can be easily run locally with docker.

Mailpit

docker run -d -p 8025:8025 -p 1025:1025 axllent/mailpit

smtp4dev

docker run -d -p 1080:80 -p 1025:25 rnwood/smtp4dev

maildev

docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev

Configure the SMTP integration

The SMTP connection details can be configured via your app's config files, and via env vars or an AsyncSource for sensitive connection details. Below is a sample config file that can be used to connect to a locally-hosted SMTP service.

# Note: Hard-coded connection details are used here for demonstration purposes only. In a real application,
# an `AsyncSource` should be used to fetch secrets from an external service, such as AWS or GCS
# secrets manager services.

[email]
from = "no-reply@example.com"

[email.smtp.connection]
# The `smtps` scheme should be used in production
uri = "smtp://localhost:1025"
# Alternatively, provide connection details as individual fields
#host = "smtp.example.com"
#port = 465
#username = "username"
#password = "password"

Sending plaintext emails

The easiest way to send an email is to simply send a plaintext email.

pub struct EmailConfirmationPlainText {
    state: AppContext,
}

impl EmailConfirmationPlainText {
    pub fn new(state: &AppContext) -> Self {
        Self {
            state: state.clone(),
        }
    }
}

#[derive(Debug, TypedBuilder, Serialize, Deserialize)]
pub struct EmailConfirmationPlainTextArgs {
    user_id: Uuid,
}

#[async_trait]
impl Worker<EmailConfirmationPlainTextArgs> for EmailConfirmationPlainText {
    #[instrument(skip_all)]
    async fn perform(&self, args: EmailConfirmationPlainTextArgs) -> sidekiq::Result<()> {
        let user = User::find_by_id(&self.state, args.user_id).await?;

        send_email(&self.state, &user).await?;

        Ok(())
    }
}

/// Send the verification email to the user.
async fn send_email(state: &AppContext, user: &User) -> RoadsterResult<()> {
    let verify_url = "https://exaple.com?verify=1234";

    let body = body(&user.name, verify_url);

    let email: MessageBuilder = (&state.config().email).into();
    let email = email
        .to(Mailbox::from_str(&user.email)?)
        .subject("Please confirm your email address")
        // Set the content type as plaintext
        .header(ContentType::TEXT_PLAIN)
        .body(body)?;

    state.smtp().send(&email)?;

    info!(user=%user.id, "Email confirmation sent");
    Ok(())
}

/// Build the plaintext email content.
fn body(name: &str, verify_url: &str) -> String {
    format!(
        r#"Hello {name},
        
        Please open the below link in your browser to verify your email:
        
        {verify_url}
        "#
    )
}

Sending html emails with Leptos

In order to send HTML content via SMTP, you can either manually write your HTML, or use something like Leptos. Leptos is a reactive Rust UI framework, but it can also be used as a simple HTML templating system. It may be possible to use other frameworks such as Yew or Dioxus as well.

The below example is the same as the plaintext example, except it formats the email message with HTML using Leptos.

pub struct EmailConfirmationHtml {
    state: AppContext,
}

impl EmailConfirmationHtml {
    pub fn new(state: &AppContext) -> Self {
        Self {
            state: state.clone(),
        }
    }
}

#[derive(Debug, TypedBuilder, Serialize, Deserialize)]
pub struct EmailConfirmationHtmlArgs {
    user_id: Uuid,
}

#[async_trait]
impl Worker<EmailConfirmationHtmlArgs> for EmailConfirmationHtml {
    #[instrument(skip_all)]
    async fn perform(&self, args: EmailConfirmationHtmlArgs) -> sidekiq::Result<()> {
        let user = User::find_by_id(&self.state, args.user_id).await?;

        send_email(&self.state, &user).await?;

        Ok(())
    }
}

/// Send the verification email to the user.
async fn send_email(state: &AppContext, user: &User) -> RoadsterResult<()> {
    let verify_url = "https://exaple.com?verify=1234";

    let body = body(&user.name, verify_url);

    let email: MessageBuilder = (&state.config().email).into();
    let email = email
        .to(Mailbox::from_str(&user.email)?)
        .subject("Please confirm your email address")
        // Set the content type as html
        .header(ContentType::TEXT_HTML)
        .body(body.to_html())?;

    state.smtp().send(&email)?;

    info!(user=%user.id, "Email confirmation sent");
    Ok(())
}

/// Build the email body as HTML using Leptos.
fn body(name: &str, verify_url: &str) -> impl IntoView {
    view! {
        <div>
            <p>"Hello "{name}","</p>
            <p>"Please click the link below to confirm your email address."</p>
            <a href=verify_url rel="noopener noreferrer">
                "Verify email"
            </a>
        </div>
    }
}

Sendgrid

Sendgrid is a popular provider for sending email with a visual email template builder. When the email-sendgrid feature is enabled, Roadster will initialize a Sendgrid client via the sendgrid crate.

Sendgrid also supports sending emails via SMTP. For details on Roadster's SMTP integration, see the previous chapter.

Configure the Sendgrid integration

The Sendgrid connection details can be configured via your app's config files, and via env vars or an AsyncSource for sensitive connection details.

# Note: Hard-coded connection details are used here for demonstration purposes only. In a real application,
# an `AsyncSource` should be used to fetch secrets from an external service, such as AWS or GCS
# secrets manager services.

[email.sendgrid]
api-key = "api-key"
# `sandbox` should not be true in prod (simply omit this field in the prod config)
sandbox = true

Sending an email

With the Sendgrid client, emails are sent by providing a Sendgrid email template ID and the template's parameters. Below is a simple example of using the Sendgrid client. See Sendgrid's docs for more details on the other fields you may want to set when sending emails.

pub struct EmailConfirmationSendgrid {
    state: AppContext,
}

impl EmailConfirmationSendgrid {
    pub fn new(state: &AppContext) -> Self {
        Self {
            state: state.clone(),
        }
    }
}

#[derive(Debug, TypedBuilder, Serialize, Deserialize)]
pub struct EmailConfirmationSendgridArgs {
    user_id: Uuid,
}

#[async_trait]
impl Worker<EmailConfirmationSendgridArgs> for EmailConfirmationSendgrid {
    #[instrument(skip_all)]
    async fn perform(&self, args: EmailConfirmationSendgridArgs) -> sidekiq::Result<()> {
        let user = User::find_by_id(&self.state, args.user_id).await?;

        send_email(&self.state, &user).await?;

        Ok(())
    }
}

const TEMPLATE_ID: &str = "template-id";

#[derive(Serialize)]
struct EmailTemplateArgs {
    verify_url: String,
}

/// Send the verification email to the user.
async fn send_email(state: &AppContext, user: &User) -> RoadsterResult<()> {
    let verify_url = "https://exaple.com?verify=1234".to_string();

    let personalization = Personalization::new(Email::new(&user.email))
        .set_subject("Please confirm your email address")
        .add_dynamic_template_data_json(&EmailTemplateArgs { verify_url })?;

    let message = Message::new(Email::new(state.config().email.from.email.to_string()))
        .set_template_id(TEMPLATE_ID)
        .add_personalization(personalization);

    state.sendgrid().send(&message).await?;

    info!(user=%user.id, "Email confirmation sent");
    Ok(())
}

Observability

Observability is an important component of any web service in order to monitor the health of the system and investigate when things go wrong. Observability includes things such as traces (or logs) and metrics. Roadster recommends emitting traces using Tokio's tracing crate and provides a default tracing configuration. If the otel feature is enabled, Roadster also supports exporting traces and metrics via OpenTelemetry, which enables viewing traces and metrics in any observability platform that supports OpenTelemetry, such as Grafana or SigNoz.

Tracing with Tokio's tracing crate

Roadster provides support for tracing as defined by the init_tracing method, which is used in the default implementation of App#init_tracing. Some of the initialization logic can be configured via the app's config files. The app can also provide its own custom tracing initialization logic.

If the otel feature is enabled, this method will also initialize some OpenTelemetry resources. See the OpenTelemetry chapter for more details.

Configuration

[tracing]
level = "debug"
format = "pretty"

See the Tracing config struct for the full list of available fields.

Custom initialization logic

If the app has custom requirements for tracing / metrics, custom logic can be provided. Note that if a custom implementation is provided, none of the default tracing setup from init_tracing will be applied.

fn build_app() -> RoadsterApp<AppContext> {
    RoadsterApp::builder()
        .tracing_initializer(|config| {
            // Implement custom logic here. See Roadster's implementation
            // for an example: https://docs.rs/roadster/latest/roadster/tracing/fn.init_tracing.html
            todo!("Custom tracing initialization logic")
        })
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        .add_service_provider(move |_registry, _state| {
            Box::pin(async move { todo!("Add services here.") })
        })
        .build()
}

Provided trace events/logic for HTTP requests

When the http feature is enabled, Roadster provides some additional tracing features via Axum/Tower middleware.

HTTP Request ID

Roadster can generate an ID for each HTTP request. This is done with the SetRequestIdMiddleware, which is a wrapper around tower-http's SetRequestIdLayer. This layer will generate a new request ID (as a UUID) for each HTTP request if one wasn't provided in the request.

Additionally, Roadster allows your app to propagate the request ID to any service it calls. This is done with the PropagateRequestIdMiddleware, which is a wrapper around tower-http's PropagateRequestIdLayer.

The request ID is fetched and propagated via an HTTP request header, which is configurable via the header-name field of each middleware's config.

[service.http.middleware.set-request-id]
header-name = "request-id"

[service.http.middleware.propagate-request-id]
header-name = "request-id"

Each of these middlewares can also be disabled is desired.

HTTP request/response events

In addition to generating an ID for each request, Roadster can also create a tracing span for each request. This is done with the TracingMiddleware, which is a wrapper around tower-http's TraceLayer, with some custom logic for how to create spans and emit events on request and response.

The span includes the request ID as an attribute. The ID is retrieved from the header name configured for the SetRequestIdMiddleware. Below is a sample trace (as logs):

2025-03-20T08:22:01.257662Z  INFO roadster::service::http::middleware::tracing: started processing request, version: HTTP/1.1, url.path: /api/_health, request_headers: {"host": "localhost:3000", "user-agent": "<redacted>", "accept": "application/json, text/plain, */*", "accept-language": "en-US,en;q=0.5", "accept-encoding": "gzip, deflate, br, zstd", "connection": "keep-alive", "referer": "http://localhost:3000/api/_docs", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "dnt": "1", "sec-gpc": "1", "priority": "u=0", "request-id": "9727c5c8-a982-42ef-8d7e-c3e388d378ae"}
    at src/service/http/middleware/tracing/mod.rs:141
    in roadster::service::http::middleware::tracing::http_request with http.request.method: GET, http.route: /api/_health, request_id: 9727c5c8-a982-42ef-8d7e-c3e388d378ae

2025-03-20T08:22:01.269580Z  INFO tower_http::trace::on_response: finished processing request, latency: 12 ms, status: 200, response_headers: {"content-type": "application/json", "request-id": "9727c5c8-a982-42ef-8d7e-c3e388d378ae", "vary": "origin, access-control-request-method, access-control-request-headers"}
    at /Users/<redacted>/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.6.2/src/trace/on_response.rs:114
    in roadster::service::http::middleware::tracing::http_request with http.request.method: GET, http.route: /api/_health, request_id: 9727c5c8-a982-42ef-8d7e-c3e388d378ae, http.response.status_code: 200

HTTP request/response payload logging for debugging

In non-prod environments, it can be very helpful to be able to inspect request and response payloads. Roadster allows this via the custom RequestResponseLoggingMiddleware. Note that this middleware does not work for requests/responses that are long-running streams.

Technically it's possible to enable this middleware in prod via app configs; however, this is strongly discouraged as this can leak sensitive data into your trace events and/or logs. If logging request/response payloads in prod is desired, the payload should be encrypted before it's logged. Alternatively, something like the secrecy crate could be used for sensitive struct fields, then the rest of the struct can be safely logged from your API handler.

OpenTelemetry

If the otel feature is enabled, Roadster will initialize various OpenTelemetry resources in the init_tracing, which is used in the default implementation of App#init_tracing. In particular, Roadster will set the OTEL service name and version and configure trace and metrics exporters.

Sample OTEL configuration

[tracing]
# Explicitly provide the service name. If not provided, will
# use the `app.name` config field, converted to `snake_case`.
service-name = "example-service-name"
trace-propagation = true
otlp-endpoint = "localhost:1234"
# Export metrics every 1 minute. Adjust based on your app's needs.
metrics-export-interval = 60000

# Set the endpoint to use as a fallback if the trace/metric endpoint is not provided
[tracing.otlp.endpoint]
protocol = "grpc"
url = "http://localhost:4317"

# Set the endpoint to use for traces
[tracing.otlp.trace-endpoint]
# The `http` protocol is enabled by default when the `otel` feature is enabled.
protocol = "http"
url = "http://localhost:4318/v1/traces"
# Traces can also be exported via grpc
#protocol = "grpc"
#url = "http://localhost:4317"

[tracing.otlp.metric-endpoint]
# The `grpc` protocol requires the `otel-grpc` feature to be enabled.
protocol = "grpc"
url = "http://localhost:4317"
# Metrics can also be exported via http
#protocol = "http"
#url = "http://localhost:4318/v1/metrics"

View metrics and traces locally

You can also view traces locally using, for example, Jaeger, Grafana, or SigNoz.

Jaeger

Probably the easiest way to view OpenTelemetry Traces locally is by running Jaeger. Jaeger only supports traces, however. To visualize metrics, use one of the other options mentioned in this section.

  1. Configure your OTLP endpoint in your app's configs. An example is provided above.
  2. Run the following command:
    docker run --rm --name jaeger \
     -p 16686:16686 \
     -p 4317:4317 \
     -p 4318:4318 \
     -p 5778:5778 \
     -p 9411:9411 \
     jaegertracing/jaeger:2.4.0
    
  3. Navigate to the UI, which is available at localhost:16686.

Grafana

Another option to view traces and metrics locally is to run Grafana's "LGTM" docker image, which is a pre-built image intended for use in development environments. It's not intended for production use, but is useful for viewing traces and metrics locally.

  1. Configure your OTLP endpoint in your app's configs. An example is provided above.
  2. Run the following command:
    docker run -p 4000:3000 -p 4317:4317 -p 4318:4318 --rm -ti grafana/otel-lgtm
    
  3. Navigate to the UI, which is available at localhost:4000.

Signoz

Another option to view traces and metrics locally is to run Signoz.

  1. Configure your OTLP endpoint in your app's configs. An example is provided above.
  2. Install and run Signoz in a directory of your choice
    # Clone the repo
    git clone -b main https://github.com/SigNoz/signoz.git && cd signoz/deploy/
    # Remove the sample application: https://signoz.io/docs/operate/docker-standalone/#remove-the-sample-application-from-signoz-dashboard
    vim docker/clickhouse-setup/docker-compose.yaml
    # Remove the `services.hotrod` and `services.load-hotrod` sections, then exit `vim`
    # Run the `docker compose` command
    ./install.sh
    
  3. Navigate to the UI, which is available at localhost:3301.
  4. To stop Signoz, run the following:
    docker compose -f docker/clickhouse-setup/docker-compose.yaml stop
    

CLI

When the cli feature is enabled, Roadster provides some CLI commands that are built into the app. Custom CLI commands can also be added using clap.

CLI commands run after the app is prepared (e.g., health checks, lifecycle handlers, and services registered, etc), and before the health checks, lifecycle handlers, and services are run. This means that the app needs to have a valid configuration in order to run a CLI command, but otherwise the app's resources (e.g. DB, Redis) don't need to be healthy (unless, of course, the specific CLI command requires the resource).

Commands provided by Roadster

Roadster defines some top-level CLI options that can apply to all commands, but otherwise everything provided by Roadster is scoped under the roadster sub-command and its alias r.

Adding custom CLI commands

Custom CLI commands and options can be defined using clap. Custom commands can be provided either at the top level, or scoped a sub-command, similar to how Roadster's commands are scoped.

Once the custom commands are defined using clap, the RunCommand trait needs to be implemented for the top-level command struct. This is what allows Roadster to invoke your custom CLI commands.

/// CLI example: Commands specific to managing the `cli-example` app are provided in the CLI
/// as well. Subcommands not listed under the `roadster` subcommand are specific to `cli-example`.
#[derive(Debug, Parser)]
#[command(version, about)]
#[non_exhaustive]
pub struct AppCli {
    #[command(subcommand)]
    pub command: Option<AppCommand>,
}

#[async_trait]
impl RunCommand<App, AppContext> for AppCli {
    #[allow(clippy::disallowed_types)]
    async fn run(&self, prepared_app: &CliState<App, AppContext>) -> RoadsterResult<bool> {
        if let Some(command) = self.command.as_ref() {
            command.run(prepared_app).await
        } else {
            Ok(false)
        }
    }
}

/// App specific subcommands
///
/// Note: This doc comment doesn't appear in the CLI `--help` message.
#[derive(Debug, Subcommand)]
pub enum AppCommand {
    /// Print a "hello world" message.
    HelloWorld,
}

#[async_trait]
impl RunCommand<App, AppContext> for AppCommand {
    async fn run(&self, _prepared_app: &CliState<App, AppContext>) -> RoadsterResult<bool> {
        match self {
            AppCommand::HelloWorld => {
                info!("Hello, world!");
            }
        }
        Ok(true)
    }
}

Sample CLI help text for the above example

$> ROADSTER__ENVIRONMENT=dev cargo run -- -h
A "Batteries Included" web framework for rust designed to get you moving fast.

CLI example: Commands specific to managing the `cli-example` app are provided in the CLI as well. Subcommands not listed under the `roadster` subcommand are specific to `cli-example`

Usage: cli-example [OPTIONS] [COMMAND]

Commands:
  roadster     Roadster subcommands. Subcommands provided by Roadster are listed under this subcommand in order to avoid naming conflicts with the consumer's subcommands [aliases: r]
  hello-world  Print a "hello world" message  
  help         Print this message or the help of the given subcommand(s)

Options:
  -e, --environment <ENVIRONMENT>      Specify the environment to use to run the application. This overrides the corresponding environment variable if it's set [possible values: development, test, production, <custom>]
      --skip-validate-config           Skip validation of the app config. This can be useful for debugging the app config when used in conjunction with the `print-config` command
      --allow-dangerous                Allow dangerous/destructive operations when running in the `production` environment. If this argument is not provided, dangerous/destructive operations will not be performed when running in `production`
      --config-dir <CONFIG_DIRECTORY>  The location of the config directory (where the app's config files are located). If not provided, will default to `./config/`
  -h, --help                           Print help
  -V, --version                        Print version

Health checks

Roadster allows registering HealthChecks to ensure server instances are functioning as expected. Roadster provides some default health checks that simply check that the server's dependencies (e.g., DB and Redis) are accessible. All health checks -- both the defaults and any custom ones registered for an app -- run on app startup, via the CLI, and in the API at /api/_health. The route of this API is configurable via the service.http.default-routes.health.route config field.

Roadster also provides the /api/_ping API, which simply returns a successful HTTP status (200) and does no other work.

Custom HealthCheck

To provide a custom health check, implement the HealthCheck trait and register the check when building the app. Note that if the check requires access to the app's state, it should be provided via a Weak reference to the state. This is because health checks are stored in Roadster's AppContext, which introduces a circular reference between the context and health checks. A weak reference to AppContext can be retrieved via AppContext#downgrade.

Implement HealthCheck

pub struct ExampleCheck {
    state: AppContextWeak,
}

impl ExampleCheck {
    pub fn new(state: &AppContext) -> Self {
        Self {
            state: state.downgrade(),
        }
    }
}

#[async_trait]
impl HealthCheck for ExampleCheck {
    fn name(&self) -> String {
        "example".to_string()
    }

    fn enabled(&self) -> bool {
        // Custom health checks can be enabled/disabled via the app config
        // just like built-in checks
        if let Some(state) = self.state.upgrade() {
            state
                .config()
                .health_check
                .custom
                .get(&self.name())
                .map(|config| config.common.enabled(&state))
                .unwrap_or_else(|| state.config().health_check.default_enable)
        } else {
            false
        }
    }

    async fn check(&self) -> RoadsterResult<CheckResponse> {
        Ok(CheckResponse::builder()
            .status(Status::Ok)
            .latency(Duration::from_secs(0))
            .build())
    }
}

Register custom HealthCheck

fn build_app() -> RoadsterApp<AppContext> {
    RoadsterApp::builder()
        // Use the default `AppContext` for this example
        .state_provider(|context| Ok(context))
        // Register custom health check(s)
        .add_health_check_provider(|registry, state| {
            registry.register(ExampleCheck::new(state))?;
            Ok(())
        })
        .add_service_provider(move |_registry, _state| {
            Box::pin(async move { todo!("Add services here.") })
        })
        .build()
}

Lifecycle hooks

See:

Testing

See:

Adding a UI

Currently, Roadster is focused on back-end API development with Rust. We leave it to the consumer to decide how they prefer to add a front-end, e.g., using an established JS/TS framework (React / Next / Vue / Svelte / Solid / etc) or using a Rust front-end framework (Leptos / Yew / Perseus / Sycamore / etc). That said, we do have some examples of how to use Roadster with some these frameworks.

Examples

FrameworkExample
Leptosleptos-ssr

Adding a UI with Leptos

For an example of how to use Leptos with Roadster, see our Leptos examples:

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

0.7.0-beta.4 - 2025-03-28

Added

  • [breaking] Follow OTEL conventions for http spans and events (#710)
  • Allow providing trace env filter directives in app config (#706)
  • Enable "head sampling" for OTEL traces (#703)

Other

  • [breaking] Mark tracing middleware structs non-exhaustive (#713)
  • Update dependencies (#708)

0.7.0-beta.3 - 2025-03-23

Added

  • [breaking] Allow either http or grpc OTLP endpoints (#701)

0.7.0-beta.2 - 2025-03-15

Fixed

  • Make AnyInitializer#stage field non-optional (#676)

Other

  • Update leptos-ssr example to leptos-0.8 and update various dependencies (#689)
  • Various updates to the documentation + book + book examples

0.7.0-beta.1 - 2025-03-04

Added

  • Allow logging sensitive headers in the dev environment (#666)
  • Allow overriding config fields or entire config (#661)
  • Accept Into<String> for ConfigOverrideSource builder (#670)

Fixed

  • Export TestAppState to allow for external use (#672)

Other

  • Update to rust 2024 edition + rustfmt 2024 style edition (#662)

0.7.0-beta - 2025-02-25

This is the first beta release for version 0.7.0. From here until the stable 0.7.0 release, the focus will be on improving docs and internal clean up. Semver breaking changes are not expected going forward for 0.7.0, but are still possible.

Other

  • Update OTEL dependencies (#659)

0.7.0-alpha.8 - 2025-02-25

Added

  • [breaking] Allow customizing the Diesel pool connections (#656)

0.7.0-alpha.7 - 2025-02-25

Other

  • [breaking] Refactor app runners and change command + lifecycle params (#652)
  • minor re-word in db book chapter (#651)

0.7.0-alpha.6 - 2025-02-24

Added

  • Create temporary databases for tests (#645)
  • [breaking] Add test hook (#643)

Fixed

  • [breaking] Fix run_test* to skip CLI (#647)

Other

  • Fix powerset checks (#648)

0.7.0-alpha.5 - 2025-02-20

Added

  • Support mysql test container (#636)

Fixed

  • Use db-sql instead of db-sea-orm where appropriate (#637)

Other

  • Remove unnecessary Empty for RoadsterApp Cli type (#633)
  • Add diesel to loco comparison page (#632)
  • Update db chapter of book (#631)

0.7.0-alpha.4 - 2025-02-18

This is a very large release with a lot of breaking changes. See the below changelog the detailed commit history. In summary, this release adds support for the Diesel SQL ORM. Diesel is a very different ORM compared to SeaORM (the ORM we currently support), and as such this release required a lot of refactoring in order to provide a relatively consistent experience regardless of which ORM a consumer decides to use (or if they decide to use both, which is possible but not particularly recommended). The refactor also resulted in some general simplifications and improvements to the devx; read on for more details. Some breaking changes include:

  • Remove the M associated type from the App trait. A Migrator can now be provided via the migrators method instead.
  • Similarly, remove the M type parameter from RoadsterApp. SeaORM, Diesel, or a generic Migrator can now be provided via the builder methods.
  • Change RunCommand#run to take a single PreparedApp struct
  • ^This allowed removing the CLI handler method from the AppService trait. CLI's now have access to the ServiceRegistry from the PreparedApp, so they can get access to a particular AppService using ServiceRegistry#get (assuming it was registered).
  • Consolidate DB migration CLI commands to provide consistent experience between SeaORM and Diesel. This also removed some slightly redundant commands.
  • Rename AppContext#db to AppContext#sea_orm
  • Rename App#db_connection_options to App#sea_orm_connection_options, and rename the related methods in RoadsterApp
  • Move/rename the DB health check
  • Add Sized as a parent trait for the App

This release also includes the following non-breaking changes:

  • Add AppContext methods to get various Diesel connection pools types, including Postgres, Mysql, Sqlite, and async pools for Postgres and Mysql. Due to Diesel's type strategy for connections, there isn't a single "DbConnection" like there is in SeaORM, so we provide individual methods depending on which feature flags are enabled.
  • Allow providing AsyncSource implementations to use with the config crate. This allows, for example, loading secret config values from an external service, such as AWS or GCS secrets managers.
  • Add a couple db config fields, test-on-checkout and retry-connection
  • Add more variants to our custom Error type

Added

  • [breaking] Add diesel support (#626)
  • [breaking] Add db-sea-orm feature to prepare for other DB crate support (#612)

Fixed

  • Kebab case for environment env var instead of lowercase (#614)

Other

  • [breaking] Replace native-tls with rustls in several dependencies (#621) thanks to @tomtom5152
  • [breaking] Remove AppContext::mailer method in favor of the smtp method (#613)
  • Remove leptos-0.6 example so to maintain a single leptos example (#610)
  • Add doc comments for Provide and ProvideRef and add to book (#598)
  • Minor improvement to initializing health checks in state (#593)
  • Refactor RoadsterApp to reduce duplication (#589)
  • Add example of using tower/axum oneshot to test APIs (#587)
  • Improve test coverage (#582)
  • Update the validator trait (#585)
  • Various documentation + test improvements

0.7.0-alpha.3 - 2025-01-22

Added

  • [breaking] Enable fetching concrete service from registry via downcast (#580)

Other

  • [breaking] Rename/move the health_check mod to health::check (#578)
  • [breaking] Remove the App#graceful_shutdown method (#577)
  • Add ExampleHealthCheck to the full example (#576)

0.7.0-alpha.2 - 2025-01-18

Added

  • Map Error::Auth to StatusCode::UNAUTHORIZED HTTP response (#571)
  • [breaking] Return RedisEnqueue and RedisFetch redis pool "new-types" (#568)

Other

  • Remove todos (#570)
  • [breaking] Remove From<Environment> impl for &'static str (#569)
  • Declare all dependencies in workspace (#567)
  • [breaking] Remove deprecated items (#566)

0.7.0-alpha.1 - 2025-01-17

Added

  • [breaking] Allow registering Worker instead of requiring AppWorker (#564)
  • Increase the default cache-control max-age to 1 week (#559)

Fixed

  • Use Router#fallback_service in NormalizePathInitializer (#562)

Other

  • Update config to 0.15.6 (#560)

0.7.0-alpha - 2025-01-14

Added

  • Support sidekiq balance strategy and dedicated queues (#543)

Fixed

  • [breaking] Propagate Validate calls to all app config fields (#557)

Other

  • [breaking] Update bb8 to v0.9 and sidekiq-rs to 0.13.1 (#555)
  • Add some details to config book page (#553)
  • Upgrade various crates that are used internally (#551)
  • [breaking] Upgrade Axum to 0.8 and Aide to 0.14 (#548)
  • Enable nightly coverage feature (#545)

0.6.24 - 2024-12-28

Added

  • Add cache-related middlewares (#541)
  • Add timestamps for when email change is confirmed (#537)
  • Add User column to store the user's new email before it's confirmed (#536)

0.6.23 - 2024-12-24

Added

  • Enable redacting timestamps from insta snapshots (#532)
  • Add AppWorker#enqueue_delayed (#531)

0.6.22 - 2024-12-07

Added

  • Add AppContextWeak to prevent reference cycles (#529)

0.6.21 - 2024-12-01

Other

  • Update OTEL patch version and remove a deprecated fn call (#527)
  • Update Loco comparisons and add some links to other sections (#522)
  • Add mailpit to SMTP dev server examples (#521)
  • (deps) bump codecov/codecov-action from 4 to 5 (#517)
  • Upgrade otel/tracing dependencies (#516)

0.6.20 - 2024-11-17

Added

  • Enable converting roadster::Error to sidekiq::Error (#514)

Other

  • Use MockProvideRef<DatabaseConnection> in an example test (#513)

0.6.19 - 2024-11-16

Added

  • Provide and ProvideRef traits to provide AppContext objects (#510)

0.6.18 - 2024-11-15

Added

  • Add support for redacting postgres/redis/smtp URIs (#507)

Other

  • Add smtp4dev to example local SMTP servers (#506)

0.6.17 - 2024-11-12

Added

  • Add support for TestContainers (pgsql + redis modules) (#503)

Other

  • Update thiserror to 2.x (#499)
  • Update validator crate (#497)

0.6.16 - 2024-10-28

Added

  • Add config to specify the domain where the service is hosted (#490)

0.6.15 - 2024-10-22

Added

  • Case-insensitive username and email fields (#480)

0.6.14 - 2024-10-21

Added

  • Add AnyMiddleware to minimize boilerplate for Axum middleware (#472)
  • Add AnyIntializer to minimize boilerplate for Axum Router initializers (#475)

Other

  • Add leptos-0.7 example (#465)

0.6.13 - 2024-10-19

Fixed

  • Only attempt to load yaml files when config-yaml is enabled (#451)

Other

  • Use FromRef from axum-core instead of axum (#450)

0.6.12 - 2024-10-17

Added

  • Enable writing config files in YAML (#446)

0.6.11 - 2024-10-17

Fixed

  • Fix trace message for health checks on startup (#443)

Other

  • Add loco comparison (#444)

0.6.10 - 2024-10-16

Added

  • Enable consumers to provide custom Environment values (#439)

0.6.9 - 2024-10-15

Added

  • Add AppContext::smtp method to alias to AppContext::mailer (#409)
  • Create documentation website using mdbook. The website can be found at roadster.dev.

Other

  • Update sea-orm (#434)
  • Create SECURITY.md (#420)
  • Create CODE_OF_CONDUCT.md (#419)

0.6.8 - 2024-10-11

Added

  • Allow configuring which req/res body content types to log (#407)

0.6.7 - 2024-10-11

Added

The main feature included in this release is support for sending emails via Sendgrid's Mail Send API. See the below items for more details.

  • Set sandbox mode on Sendgrid message based on config (#403)
  • Add Sendgrid client to AppContext (#402)
  • Add support to config for email via Sendgrid (email-sendgrid feature) (#401)

Other

  • Add note to readme about supporting Sendgrid (#405)
  • Add example of using Sendgrid client (#404)

0.6.6 - 2024-10-10

Added

  • Allow configuring the interval at which metrics are exported (#399)

0.6.5 - 2024-10-09

Added

The main feature included in this release is support for sending emails via SMTP. See the below items for more details.

  • Add SmtpHealthCheck (#396)
  • Allow specifying the smtp port via config (#395)
  • Add smtp client to AppContext (#391)
  • Add support to config for email via SMTP (email-smtp feature) (#388)

Fixed

  • Fix config value used for timeout of health check in api and cli (#397)

Other

  • Add example of sending email using lettre smtp client (#394)
  • Add doc comment explaining how NormalizePathLayer works (#393)

0.6.4 - 2024-10-05

Other

  • Update dependencies (#386)
  • Disable default features for rstest (#380)

0.6.3 - 2024-09-15

Added

  • Add more builder methods for RoadsterApp (#370)
  • Builder-style API for App (#367)

Other

  • Add logs for successful health checks (#371)

0.6.2 - 2024-08-30

Added

  • Allow specifying a custom config dir (#361)
  • Add lifecycle handlers (#360)

0.6.1 - 2024-08-28

Added

  • Allow running CLI commands without requiring DB/Redis connections (#353)

Other

  • Update typed-builder and several examples' dependencies (#352)

0.6.0 - 2024-08-25

Added

  • Add a public method to decode a JWT from a string (#348)
  • Mark refresh token headers as sensitive (#347)
  • Make the User sea-orm migration enum public (#346)
  • Allow splitting config files into many files in env directories (#344)
  • [breaking] App methods take self (#337)
  • Remove cookie extraction for Jwt, but allow it in JwtCsrf (#332)
  • Allow custom sub-claims in provided Claims types (#331)
  • Allow jwt from cookie, but only if it's explicitly requested (#329)

Fixed

  • [breaking] Don't expect a "Bearer" token in the auth token cookie (#340)

Other

  • Update leptos example to use site-addr and env from roadster config (#341)
  • sea-orm workspace dep and upgrade to 1.0.0 (#336)
  • [breaking] Update tower to 0.5.0 (#334)

0.5.19 - 2024-08-12

Added

  • Redact bearer tokens in insta snapshots (#325)

Fixed

  • Do not simply use bearer token from cookie for auth (#326)
  • Derive Clone in JWT claim types (#323)
  • Implement From for various Subject enum variants (#323)
  • Use leptos_routes in leptos example instead of leptos_routes_with_context (#322)

Other

  • (deps) Bump EmbarkStudios/cargo-deny-action from 1 to 2 (#319)

0.5.18 - 2024-08-05

Other

  • Update rstest dependency (#318)

0.5.17 - 2024-08-05

Fixed

  • Extract jwt as a bearer token from cookies (#316)

0.5.16 - 2024-08-04

Added

  • Extract JWT from cookie (#314)
  • Derive OperationIo for Jwt struct (#311)
  • Change user.last_sign_in_at column to non-null with default (#312)

Other

  • Add pre-commit hook to check formatting (#313)

0.5.15 - 2024-08-01

Added

  • Allow configuring the max len for the ReqResLoggingMiddleware (#309)

0.5.14 - 2024-08-01

Added

  • Enable ReqResLogging middleware by default, but disable in prod (#307)

0.5.13 - 2024-07-31

Added

  • Add middleware to log the request/response payloads (#304)
  • Log errors at debug level in IntoResponse impl (#303)

0.5.12 - 2024-07-29

Added

  • PasswordUpdatedAt column + auto-update with a fn and trigger (#301)

0.5.11 - 2024-07-26

Added

  • Migration to enable the uuid-ossp Postgres extension (#297)
  • Add non-pk versions of uuid schema helper methods (#296)

0.5.10 - 2024-07-25

Added

  • Use IDENTITY column for int primary keys instead of BIGSERIAL (#293)

Fixed

  • Add "if exists" to user's drop_table migration statement (#292)

Other

  • Add tests for schema and check helper methods (#289)

0.5.9 - 2024-07-24

Added

  • Auto-update timestamp columns (#287)
  • Add SeaORM migrations and utils to create user table (#284)

Other

  • Disallow unwrap and expect except in tests (#286)

0.5.8 - 2024-07-22

Other

  • Remove the update justfile command (#282)
  • Use the main project README.md as the library's top-level docs (#281)

0.5.7 - 2024-07-22

Other

  • Update dependencies (#279)

0.5.6 - 2024-07-22

Added

  • Add TestCase utility for configuring insta settings (#277)

0.5.5 - 2024-07-08

Added

  • Allow configuring the tracing log output format (#275)

0.5.4 - 2024-07-07

Added

  • Add method to prepare the app separately from running it (#270)

Fixed

  • Correctly add the ApiRouter to the HTTP service's ApiRouter (#273)

Other

  • Fixes for default openapi docs (#271)

0.5.3 - 2024-07-04

Other

  • Update the _health HTTP API docs (#267)

0.5.2 - 2024-07-02

Added

  • Allow configuring the max duration of health checks (#264)

0.5.1 - 2024-07-02

Added

  • Place health check results under resources in response (#261)

Other

  • Fix typos in README (#260)

0.5.0 - 2024-07-01

Added

  • [breaking] Remove interior mutability of HealthCheckRegistry (#258)

0.4.0 - 2024-07-01

Added

  • [breaking] Implement health check API using HealthCheck trait (#255)
  • [breaking] Switch to Axum's FromRef for custom state (#250)

Other

  • [breaking] Remove deprecated items in preparation of 0.4 release (#253)
  • Add example for integrating with Leptos (#252)
  • Use small number of sidekiq workers for full example in dev/test (#251)

0.3.5 - 2024-06-24

Fixed

  • Health check config is missing a custom field (#246)

Other

  • Check PR title for compliance with conventional commits (#247)

0.3.4 - 2024-06-23

Added

  • Add health checks to run before starting services (#242)
  • Add From impl to convert db config to ConnectOptions (#240)
  • Move sidekiq "stale cleanup" to new before_run service method (#239)

Other

  • Add dependabot config to update github actions weekly (#243)
  • Update READMEs to use __ as the env var separator instead of .
  • Update list of UI frameworks in readme
  • Set up cargo deny

0.3.3 - 2024-06-21

Fixed

  • Invalid env var separator on bash

Other

  • Add inclusive language check to CI
  • Fix clippy error
  • Remove non-inclusive language

0.3.2 - 2024-06-14

Other

  • Run Feature Powerset checks + perform a release twice a week
  • Add goals and future plans to readme + some getting started steps
  • Add github action to verify commits follow 'Conventional Commits' format

0.3.1 - 2024-06-11

Other

  • Implement the health check API as a protocol agnostic core module
  • Minor changes to the FunctionService doc example

0.3.0 - 2024-06-10

Other

  • Fix minimal version of serde
  • Add #[non_exhaustive] to public enums
  • Add Add #[non_exhaustive] to public structs
  • Enable grpc by default in the full example
  • Add support for tower's CORS middleware
  • Add AppMetadata struct + App::metadata method and add version to otel
  • Run doctests as part of test and test-watch just commands
  • Update readme to include grpc and generic function service
  • Update FunctionService doctest to only run with default features
  • Add a generic app service to run an async function as a service
  • Move semver checks to a separate workflow
  • Use depth 3 in feature powerset
  • Install protoc in feature powerset workflow
  • Remove the old deprecated cli mod
  • Add basic grpc example
  • Add basic support for serving a gRPC service
  • Update rstest

0.2.6 - 2024-06-03

Other

  • Add builder method to add middleware for the sidekiq processor
  • Declare minimal version of dependencies that's actually needed
  • Add cargo-minimal-versions for direct dependencies

0.2.5 - 2024-06-03

Other

  • Only test the AppConfig#test method when all (most) features are enabled
  • Hard-code the number of sidekiq workers to avoid snapshot failures
  • Ignore a clippy error
  • Ignore coverage for the AppConfig#test method
  • Provide config defaults via config files
  • Move database and tracing mods to directories
  • Upgrade dependencies

0.2.4 - 2024-05-31

Other

  • Upgrade dependencies

0.2.3 - 2024-05-27

Other

  • Remove http feature gate for api mod
  • Move cli mod to be a child of the api mod
  • Add semver checks to CI
  • Revert "Use stable rust for coverage"

0.2.2 - 2024-05-26

Other

  • Add latest version of time to workaround build issue on nightly

0.2.1 - 2024-05-26

Other

  • Add missing needs field to powerset_clippy workflow step
  • Run separate jobs for each feature powerset check

0.2.0 - 2024-05-26

Other

  • Add custom error type using thiserror
  • Fix incorrect feature flag used on import statement
  • Allow partial overrides of all configs
  • Remove mock AppContext and use a concrete version in tests instead
  • Add a small description for the validate-codecov-config just cmd
  • Use automock to mock traits instead of the mock! macro
  • Use github discussions instead of discord (for now at least)
  • Add tests for controller config methods
  • Fix exiting the app when a cli command is handled
  • Allow running the release_pr workflow manually
  • Add code owners to automatically request reviews on PRs
  • Add documentation for the TestCase utility class
  • Use stable rust for coverage
  • Add tests for sidekiq builder and roadster cli
  • Run release pr workflow with manual dispatch
  • Separate different parts of the app::start method into respective mods
  • Add tests for the DefaultRoutes config validator
  • Fix DefaultRoutes validator when open-api feature is not enabled
  • Add validation of the AppConfig
  • Add deps.rs badge to readme
  • Group http and open-api features in the feature_powerset workflow
  • Update the codecov PR comment config
  • Add tests for remove_stale_periodic_jobs
  • Update dependencies that can be updated
  • Add a feature flag to entirely disable the http service
  • Add comments to justfile
  • Update dependencies and add just command to update deps
  • Add MSRV tag to readme
  • Add MSRV and add CI step to validate
  • Add some tests for SidekiqWorkerServiceBuilder
  • Add justfile
  • Add tests for SidekiqWorkerService::enabled
  • Fix clippy warning
  • Allow unknown cfg in coverage workflow
  • Disable coverage for tests
  • Enable running coverage using the nightly toolchain
  • Add tests for the Middleware::priority methods for each middleware
  • Add tests for the Middleware::enabled method for each middleware
  • Add tests for the default_middleware and default_initializers methods
  • Add tests for middleware/initializer registration in HttpServiceBuilder
  • Don’t use coverage(off) for now because it’s unstable
  • Use coverage instead of coverage_nightly
  • Apply coverage(off) directly to the desired method
  • Use coverage(off) only with cfg_attr(coverage)
  • Disable coverage for service mod test impls
  • Add small test for service builder
  • Remove async-std from dev deps
  • Remove Tokio from non-async test
  • Use Tokio for rstest tests
  • Run cargo upgrade to update dependencies
  • Update the code coverage comment format
  • Set up mocking using mockall crate
  • Do some test cleanup
  • Add test for route that isn't documented
  • Add test for the HttpService::list_routes method/cli command
  • Rename the custom context in the minimal example
  • Remove unnecessary From... impl for AppContext
  • Pass config and context by reference in all public APIs
  • Custom state as member of AppContext
  • Add methods to AppContext instead of direct field access
  • Fix codecov config
  • Add tests to serde_util
  • Add codecov config file
  • Update instructions to run CI locally
  • Add coverage badge to the readme
  • Add workflow to generate code coverage stats
  • Disallow registering things multiple times
  • Create FUNDING.yml
  • Update feature_powerset.yml schedule
  • Rearrange and enhance the status badges in the readme
  • Add a Discord badge
  • Have docs.rs pass --all-features to ensure all features have docs built

0.1.1 - 2024-05-05

Other

  • Only run the release workflow on main
  • Install missing nextest dependency for feature powerset
  • Add crates.io and docs.rs badges
  • Run the release workflow after the Feature Powerset workflow succeeds

0.1.0 - 2024-05-05

Other

  • Set publish = false in the minimal example Cargo.toml
  • Set publish = true in the Cargo.toml
  • Remove fetch depth of 0 from CI and feature powerset
  • Automate releases with release-plz
  • Use fetch depth of 0 in CI
  • Remove lazy_static dependency
  • Use the nextest test runner
  • Upgrade dependencies in Cargo.tomls
  • Fix the graceful shutdown of the sidekiq service
  • Timeout the ping redis method so the health route can return without fully timing out
  • Implement the sidekiq processor task as an AppService
  • Move middleware/initializers to service/http module
  • Add ServiceRegistry and restructure configs
  • Add AppService trait and use it to implement the HTTP service
  • Add example worker and api to the minimal example
  • Don't run Processor depending on configs
  • Use a separate redis connection pool for enqueuing vs fetching jobs
  • Remove url::Url import from app_config.rs for sidekiq feature
  • Allow configuring the number of sidekiq worker tasks
  • Remove stale periodic jobs
  • Enable registering periodic workers
  • Check disk usage between feature powerset workflow steps
  • Add defaults for AppWorkerConfig's builder
  • Add RoadsterWorker to provide common behaviors for workers
  • Add instructions for RedisInsight to the readme
  • Add standalone sidekiq dashboard instructions to readme
  • Clean between powerset build stages
  • Skip and group features to reduce powerset size
  • Use cfg feature flag instead of allowing unused import
  • Add feature flag to enable exporting traces/metrics using otel
  • Add to list of features in readme
  • Add CLI command to print the app config
  • Add CLI commands to run DB migrations
  • Add CLI command to generate an openapi schema
  • Allow private intra doc links for rustdoc
  • Add CLI command to list API routes
  • Set up roadster CLI and custom app CLI
  • Fix the cron used for feature_powerset.yml workflow
  • Remove a cfg that caused a build error
  • Add doc comment for Initializer::priority
  • Allow using custom App::State in Initializer and Middleware traits
  • Remove debugging outputs
  • Fix step names used to define outputs
  • Add missing runs-on field
  • Add debugging log to workflow
  • Add log of label name
  • Use uniq job output names
  • Fix error in feature_powerset.yml
  • Allow triggering the feature powerset check by adding a lable to a pr
  • Add missing cfg for the open-api feature
  • Fix a powerset build error
  • Add Swatinem/rust-cache@v2 to cache rust builds
  • Update checkout action version
  • Add workflow_dispatch event to feature_powerset.yml
  • Add github workflow to run checks against the powerset of features
  • Remove "all features" job b/c it's a duplicate of the cargo hack job
  • Use cargo hack --each-feature instead of --feature-powerset
  • Add openid jwt claims
  • Minor changes
  • Fix build break with all features disabled
  • Enable reporting traces/metrics via an otlp exporter
  • Use snake case in github ci job
  • Add RequestDecompressionMiddleware
  • Add more crate-level documentation
  • Create LICENSE
  • Move workspace declaration to the bottom of the Cargo.toml
  • Fix a rustdoc::all warning
  • Remove --no-dev-deps where it can't be used in github ci workflow
  • Use cargo hack to test feature powerset
  • Update cargo checks
  • Fix cargo fmt command
  • Create workspace that includes the examples
  • Set working dir for examples job
  • Add a minimal example
  • Don't run clippy against deps in ci
  • Add CI badge to the readme
  • Update checkout action to v4
  • Add workflow stage to run checks for all features
  • Add missing checkout in workflow
  • Use custom husky hooks
  • Add github workflow to run checks with all feature combinations
  • Add feature flag for generating openapi schema using aide
  • Add feature flag for the SQL db
  • Add feature flag for sidekiq
  • Add RequestBodyLimitMiddleware
  • Add TimeoutLayer middleware
  • Add instructions for generating an html coverage report
  • Use JoinSet instead of TaskTracker
  • Make the Jwt claims type generic and use Claims as the default
  • Add notes on background job queue options
  • Add JWT extractor with basic Claims impl for default/recommended claims
  • Add logs for sidekiq queues
  • Add ping latencies to health check response
  • Allow configuring the max number of redis connections
  • Don't bail early in graceful shutdown if an error occurred.
  • Minor string change
  • Remove instrument from cancel_on_error
  • Remove log from dependencies
  • Improve graceful shutdown logic
  • Always run shutdown logic and don't require consumer to run it
  • Add token cancelation drop guard, and add doc comment recommending to use the default shutdown signal
  • Add logs for installing middleware
  • Add compression middleware
  • Remove stray log
  • Add catch panic middleware
  • Add graceful shutdown signal
  • Add rusty-sidekiq for running async jobs
  • Add _health route to check the health of the service
  • Enable migrations
  • Add SeaORM integration
  • Enable custom configs for initializers
  • Add Initializer with various hooks, and add NormalizePathInitializer
  • Minor change to concat middleware vecs inline
  • Don't require consumers to include default middleware
  • Reorder default middleware -- order determined by config now
  • Enable providing configs for custom middleware
  • Enable configuring middleware
  • Add environment to the AppConfig
  • Add OpenAPI docs + spec routes
  • Add tracing middleware
  • Add request id middleware
  • Allow middleware installers to return a result
  • Enable adding middleware and provide defaults
  • Require custom state to be convertable to AppContext
  • Add default _ping route
  • Enable defining routes using Axum or Aide routers
  • Use From trait instead of a custom trait
  • Re-order dependencies in Cargo.toml
  • Add App trait and allow providing a custom state
  • Add app entrypoint
  • Init tracing
  • Add basic configuration support
  • Remove .idea directory
  • Remove Cargo.lock from git
  • Move cargo-husky to dev-deps
  • Prevent publishing for now
  • Add cargo-husky
  • Init and add empty rust lib project