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(())
}