Initial commit: Axum web service template
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# API Configuration
|
||||
{{env_prefix}}_API_KEY=your-secret-api-key-here
|
||||
|
||||
# Server Configuration
|
||||
{{env_prefix}}_HOST=127.0.0.1
|
||||
{{env_prefix}}_PORT={{default_port}}
|
||||
|
||||
# Logging
|
||||
{{env_prefix}}_LOG_LEVEL={{default_log_level}}
|
||||
3
.genignore
Normal file
3
.genignore
Normal file
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
Cargo.lock
|
||||
.env
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
.env*
|
||||
!.env.example
|
||||
1359
Cargo.lock
generated
Normal file
1359
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
Normal file
36
Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "{{project-name}}"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "{{project-description}}"
|
||||
|
||||
[dependencies]
|
||||
{% if include_aide %}aide = { version = "0.16.0-alpha.1", features = [
|
||||
"axum",
|
||||
"axum-json",
|
||||
"axum-query",
|
||||
] }
|
||||
{% endif %}anyhow = "1.0.101"
|
||||
axum = { version = "0.8.8", features = ["json", "query"] }
|
||||
axum-app-wrapper = { git = "https://gitea.fasharp.io/fa-sharp/axum-app-wrapper", rev = "a8d5e4f962" }
|
||||
{% if include_aide %}schemars = { version = "1.0", features = [
|
||||
"chrono04",
|
||||
"preserve_order",
|
||||
"url2",
|
||||
"uuid1"
|
||||
] }
|
||||
{% endif %}
|
||||
dotenvy = "0.15.7"
|
||||
figment = { version = "0.10.19", features = ["env"] }
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.145"
|
||||
tokio = { version = "1.48.0", default-features = false, features = [
|
||||
"macros",
|
||||
"net",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"signal"
|
||||
] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] }
|
||||
type-map = "0.5.1"
|
||||
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Axum Web Service Template
|
||||
|
||||
A production-ready template for building web services with Rust and Axum.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Axum** - Fast and ergonomic web framework
|
||||
- ⚙️ **Configuration Management** - Environment-based config with `figment`
|
||||
- 📝 **Structured Logging** - JSON logging in production with `tracing`
|
||||
- 🔄 **Graceful Shutdown** - Handles SIGTERM and SIGINT signals
|
||||
- 🏗️ **Plugin Architecture** - Modular app initialization with `axum-app-wrapper`
|
||||
- 🔌 **Optional OpenAPI** - API documentation with `aide` (optional)
|
||||
|
||||
## Usage
|
||||
|
||||
### Using cargo-generate
|
||||
|
||||
Install cargo-generate if you haven't already:
|
||||
|
||||
```bash
|
||||
cargo install cargo-generate
|
||||
```
|
||||
|
||||
Generate a new project from this template:
|
||||
|
||||
```bash
|
||||
cargo generate --git <your-template-repo-url>
|
||||
```
|
||||
|
||||
You'll be prompted for:
|
||||
- **Project name**: The name of your new project
|
||||
- **Project description**: A brief description
|
||||
- **Environment variable prefix**: Prefix for env vars (e.g., `APP` for `APP_HOST`, `APP_PORT`)
|
||||
- **Default port**: The server's default port
|
||||
- **Default log level**: trace, debug, info, warn, or error
|
||||
- **Include aide**: Whether to include OpenAPI documentation support
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Clone or download this repository
|
||||
2. Update `Cargo.toml` with your project name and details
|
||||
3. Copy `.env.example` to `.env` and configure your environment variables
|
||||
4. Run `cargo build`
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is loaded from environment variables. The prefix is configurable during template generation.
|
||||
|
||||
Example with `APP` prefix:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
APP_API_KEY=your-secret-key
|
||||
|
||||
# Optional (defaults shown)
|
||||
APP_HOST=127.0.0.1
|
||||
APP_PORT=8080
|
||||
APP_LOG_LEVEL=info
|
||||
```
|
||||
|
||||
In development, you can use a `.env` file (copy from `.env.example`).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, server setup
|
||||
│ ├── lib.rs # App initialization
|
||||
│ ├── config.rs # Configuration management
|
||||
│ └── state.rs # Application state
|
||||
├── Cargo.toml # Dependencies
|
||||
└── .env.example # Example environment variables
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run in development mode (loads .env file)
|
||||
cargo run
|
||||
|
||||
# Run with custom log level
|
||||
RUST_LOG=debug cargo run
|
||||
|
||||
# Build for production
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Adding Routes
|
||||
|
||||
This template uses `axum-app-wrapper` for modular initialization. To add routes:
|
||||
|
||||
1. Create a new plugin in a separate module
|
||||
2. Register it in `lib.rs`:
|
||||
|
||||
```rust
|
||||
pub async fn create_app() -> anyhow::Result<(axum::Router, AppConfig, impl Future + Send)> {
|
||||
let (router, state, on_shutdown) = App::new()
|
||||
.register(config::plugin())
|
||||
.register(your_routes::plugin()) // Add your plugin here
|
||||
.init()
|
||||
.await?;
|
||||
|
||||
let app_config = state.config.to_owned();
|
||||
Ok((router.with_state(state), app_config, on_shutdown))
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Configure your license as needed.
|
||||
28
cargo-generate.toml
Normal file
28
cargo-generate.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[template]
|
||||
cargo_generate_version = ">=0.10.0"
|
||||
|
||||
[placeholders.project-description]
|
||||
type = "string"
|
||||
prompt = "Project description?"
|
||||
default = "A Rust web service built with Axum"
|
||||
|
||||
[placeholders.env_prefix]
|
||||
type = "string"
|
||||
prompt = "Environment variable prefix (e.g., APP for APP_HOST, APP_PORT)?"
|
||||
default = "APP"
|
||||
|
||||
[placeholders.default_port]
|
||||
type = "string"
|
||||
prompt = "Default server port?"
|
||||
default = "8080"
|
||||
|
||||
[placeholders.default_log_level]
|
||||
type = "string"
|
||||
prompt = "Default log level?"
|
||||
default = "info"
|
||||
choices = ["trace", "debug", "info", "warn", "error"]
|
||||
|
||||
[placeholders.include_aide]
|
||||
type = "bool"
|
||||
prompt = "Include aide for OpenAPI documentation?"
|
||||
default = true
|
||||
48
src/config.rs
Normal file
48
src/config.rs
Normal 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
13
src/lib.rs
Normal 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
70
src/main.rs
Normal 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
39
src/state.rs
Normal 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>()))
|
||||
}
|
||||
Reference in New Issue
Block a user