Initial commit: Axum web service template

This commit is contained in:
fa-sharp
2026-02-19 15:45:45 -05:00
commit 694b94d579
11 changed files with 1719 additions and 0 deletions

48
src/config.rs Normal file
View File

@@ -0,0 +1,48 @@
use std::net::{IpAddr, Ipv4Addr};
use anyhow::Context;
use axum_app_wrapper::AdHocPlugin;
{% if include_aide %}use schemars::JsonSchema;
{% endif %}
use serde::Deserialize;
use crate::state::AppState;
#[derive(Debug, Clone, Deserialize{% if include_aide %}, JsonSchema{% endif %})]
pub struct AppConfig {
pub api_key: String,
#[serde(default = "default_host")]
pub host: IpAddr,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default = "default_log_level")]
pub log_level: String,
}
fn default_host() -> IpAddr {
IpAddr::V4(Ipv4Addr::LOCALHOST)
}
fn default_port() -> u16 {
{{default_port}}
}
fn default_log_level() -> String {
"{{default_log_level}}".to_string()
}
/// Plugin that reads and validates configuration, and adds it to server state
pub fn plugin() -> AdHocPlugin<AppState> {
AdHocPlugin::new().on_init(|mut state| async move {
let config = extract_config()?;
state.insert(config);
Ok(state)
})
}
/// Extract the configuration from env variables prefixed with `{{env_prefix}}_`.
fn extract_config() -> anyhow::Result<AppConfig> {
let config = figment::Figment::new()
.merge(figment::providers::Env::prefixed("{{env_prefix}}_"))
.extract::<AppConfig>()
.context("Failed to extract valid configuration")?;
Ok(config)
}

13
src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
use axum_app_wrapper::App;
use crate::config::AppConfig;
mod config;
mod state;
pub async fn create_app() -> anyhow::Result<(axum::Router, AppConfig, impl Future + Send)> {
let (router, state, on_shutdown) = App::new().register(config::plugin()).init().await?;
let app_config = state.config.to_owned();
Ok((router.with_state(state), app_config, on_shutdown))
}

70
src/main.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::{net::SocketAddr, str::FromStr};
use {{crate_name}}::create_app;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Read .env file in debug mode
#[cfg(debug_assertions)]
dotenvy::dotenv().ok();
// Build server
let (router, config, on_shutdown) = create_app().await?;
// Configure logging output (use JSON logs in release mode)
let level_filter = LevelFilter::from_str(&config.log_level)?;
let env_filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(level_filter.into())
.from_env_lossy();
if cfg!(debug_assertions) {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_max_level(level_filter)
.init();
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(level_filter)
.with(tracing_subscriber::fmt::layer().json().flatten_event(true))
.init();
}
// Start listening for requests
let addr = SocketAddr::new(config.host, config.port);
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("Server listening on http://{}...", listener.local_addr()?);
axum::serve(listener, router.into_make_service())
.with_graceful_shutdown(shutdown_signal(on_shutdown))
.await?;
Ok(())
}
/// Shutdown signal: listens for Ctrl-C, SIGINT, SIGTERM signals
async fn shutdown_signal(on_shutdown: impl Future + Send) {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to register Ctrl-C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to register SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("Received shutdown signal, shutting down server...");
on_shutdown.await;
}

39
src/state.rs Normal file
View File

@@ -0,0 +1,39 @@
//! Application state
use std::{any::type_name, ops::Deref, sync::Arc};
use type_map::concurrent::TypeMap;
use crate::config::AppConfig;
/// App state stored in the Axum router
#[derive(Clone)]
pub struct AppState(Arc<AppStateInner>);
pub struct AppStateInner {
pub config: AppConfig,
}
impl Deref for AppState {
type Target = AppStateInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<TypeMap> for AppState {
type Error = anyhow::Error;
fn try_from(mut map: TypeMap) -> Result<Self, Self::Error> {
Ok(Self(Arc::new(AppStateInner {
config: extract(&mut map)?,
})))
}
}
fn extract<T: 'static>(type_map: &mut TypeMap) -> anyhow::Result<T> {
type_map
.remove()
.ok_or_else(|| anyhow::anyhow!("Type not found in state: {}", type_name::<T>()))
}