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:
- Asynchronous Programming in Rust
- Tokio tutorial
- Tokio is one of the two most popular async runtimes (the other being async-std). Roadster only supports Tokio at the moment.
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.
Feature | Roadster | Loco |
---|---|---|
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 | β | β |
β | β | |
ββ³ 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'sFromRef
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:
- https://medium.com/deno-the-complete-reference/rust-actix-vs-axum-hello-world-performance-e10a1c1419e0
- https://randiekas.medium.com/rust-the-fastest-rust-web-framework-in-2024-cf738c40343b
- https://old.reddit.com/r/rust/comments/1bj9rc3/axum_or_actix_in_2024/
- https://blog.logrocket.com/top-rust-web-frameworks/#backend-web-frameworks
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:
- leptos-ssr - Leptos example
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 Source
s 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 AsyncSource
s 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.
AsyncSource
s 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
Source
sAsyncSource
s (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.
Docs.rs links
AppConfig
struct
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 HealthCheck
s, and most HealthCheck
s 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
Docs.rs links
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.
Feature | Connection type |
---|---|
db-diesel-postgres-pool | Non-async Postgres |
db-diesel-mysql-pool | Non-async Mysql |
db-diesel-sqlite-pool | Non-async Sqlite |
db-diesel-postgres-pool-async | Async Postgres |
db-diesel-mysql-pool-async | Async 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 AppService
s, 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()
}
Docs.rs links
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(())
}
Docs.rs links
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
Docs.rs links
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 theservice.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) and10,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 to0
.
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
}
Docs.rs links
Initializers
Initializer
s are similar
to
Middleware
-- they both
allow configuring the Axum Router
for your app's HTTP service.
However, Initializer
s 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 Extension
s 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
Initializer
s 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:
after_router
: Runs after all of the routes have been added to theRouter
before_middleware
: Runs before anyMiddleware
is added to theRouter
.after_middleware
: Runs after allMiddleware
has been added to theRouter
.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 Initializer
s 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 theservice.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) and10,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 to0
.
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(),
)
}
Docs.rs links
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
Worker
s.
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(())
}
Docs.rs links
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)
}
Docs.rs links
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(())
}
Docs.rs links
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:
- https://owasp.org/www-community/attacks/csrf
- https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
If the functionality to extract from a cookie is not required, itβs recommended to use the normal
Jwt
extractor directly.
Docs.rs links
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(())
}
Docs.rs links
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.
Docs.rs links
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>
}
}
Docs.rs links
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(())
}
Docs.rs links
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.
Docs.rs links
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.
- Configure your OTLP endpoint in your app's configs. An example is provided above.
- 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
- 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.
- Configure your OTLP endpoint in your app's configs. An example is provided above.
- Run the following command:
docker run -p 4000:3000 -p 4317:4317 -p 4318:4318 --rm -ti grafana/otel-lgtm
- Navigate to the UI, which is available at localhost:4000.
Signoz
Another option to view traces and metrics locally is to run Signoz.
- Configure your OTLP endpoint in your app's configs. An example is provided above.
- 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
- Navigate to the UI, which is available at localhost:3301.
- To stop Signoz, run the following:
docker compose -f docker/clickhouse-setup/docker-compose.yaml stop
Docs.rs links
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
Docs.rs links
Health checks
Roadster allows registering
HealthCheck
s 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()
}
Docs.rs links
Lifecycle hooks
See:
Testing
See:
run_test
run_test_with_result
- https://docs.rs/roadster/latest/roadster/testing/snapshot/index.html
- SeaORM MockDatabase example
- Temporary test DB config
- Database TestContainers config
- Sidekiq TestContainers config
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
Framework | Example |
---|---|
Leptos | leptos-ssr |
Adding a UI with Leptos
For an example of how to use Leptos with Roadster, see our Leptos examples:
- leptos-ssr - Leptos example
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
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>
forConfigOverrideSource
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 ofdb-sea-orm
where appropriate (#637)
Other
- Remove unnecessary
Empty
forRoadsterApp
Cli
type (#633) - Add
diesel
toloco
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 theApp
trait. AMigrator
can now be provided via themigrators
method instead. - Similarly, remove the
M
type parameter fromRoadsterApp
. SeaORM, Diesel, or a genericMigrator
can now be provided via the builder methods. - Change
RunCommand#run
to take a singlePreparedApp
struct - ^This allowed removing the CLI handler method from the
AppService
trait. CLI's now have access to theServiceRegistry
from thePreparedApp
, so they can get access to a particularAppService
usingServiceRegistry#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
toAppContext#sea_orm
- Rename
App#db_connection_options
toApp#sea_orm_connection_options
, and rename the related methods inRoadsterApp
- Move/rename the DB health check
- Add
Sized
as a parent trait for theApp
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 theconfig
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
andretry-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 thesmtp
method (#613) - Remove leptos-0.6 example so to maintain a single leptos example (#610)
- Add doc comments for
Provide
andProvideRef
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 tohealth::check
(#578) - [breaking] Remove the
App#graceful_shutdown
method (#577) - Add
ExampleHealthCheck
to thefull
example (#576)
0.7.0-alpha.2 - 2025-01-18
Added
- Map
Error::Auth
toStatusCode::UNAUTHORIZED
HTTP response (#571) - [breaking] Return
RedisEnqueue
andRedisFetch
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 requiringAppWorker
(#564) - Increase the default cache-control max-age to 1 week (#559)
Fixed
- Use
Router#fallback_service
inNormalizePathInitializer
(#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
tosidekiq::Error
(#514)
Other
- Use
MockProvideRef<DatabaseConnection>
in an example test (#513)
0.6.19 - 2024-11-16
Added
Provide
andProvideRef
traits to provideAppContext
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
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
fromaxum-core
instead ofaxum
(#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 toAppContext::mailer
(#409) - Create documentation website using mdbook. The website can be found at roadster.dev.
Other
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
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
0.6.3 - 2024-09-15
Added
Other
- Add logs for successful health checks (#371)
0.6.2 - 2024-08-30
Added
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 inJwtCsrf
(#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 variousSubject
enum variants (#323) - Use
leptos_routes
in leptos example instead ofleptos_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
forJwt
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
Other
- Disallow
unwrap
andexpect
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 configuringinsta
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'sApiRouter
(#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 forapi
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 topowerset_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
andopen-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 forAppContext
- 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.toml
s - 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 fromapp_config.rs
forsidekiq
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 ofTaskTracker
- 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
fromcancel_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