Axum with PostgreSQL and SeaORM
Create a Rust API project with Axum, PostgreSQL, SeaORM, tracing, and structured error handling using Better Fullstack.
Updated 2026-05-12
Use this stack when you want a Rust backend with a modern HTTP framework, relational persistence, logging, and explicit error handling.
npm create better-fullstack@latest my-rust-api -- \
--ecosystem rust \
--rust-web-framework axum \
--database postgres \
--rust-orm sea-orm \
--rust-logging tracing \
--rust-error-handling anyhow-thiserrorGenerated stack snapshot
The boundaries to preserve as the Rust service grows.
- HTTP
- Axum
- Typed extractors, shared router state, and explicit handlers.
- Data
- PostgreSQL + SeaORM
- Entity-style database access and active models.
- Observability
- tracing
- Structured logs from startup through request handling.
- Errors
- Anyhow + Thiserror
- Domain errors converted into HTTP responses at the edge.
What this creates
- A Rust API project using Axum.
- PostgreSQL as the database option.
- SeaORM for database access.
- Tracing for structured logging.
- Anyhow and Thiserror style error handling.
Generated shape
This stack is meant to produce a backend service, not a fullstack app. The generated project gives you a typed HTTP boundary in Axum, a database layer owned by SeaORM, and request-scoped logging through tracing.
The important implementation boundaries are:
- Routes stay thin and translate HTTP input into application calls.
- SeaORM models and entities own database mapping.
- Errors are modeled explicitly, then converted into HTTP responses at the edge.
- Configuration is read once at startup so database pools, routers, and logging can be wired predictably.
Representative file tree
The exact filenames may change as the generator evolves, but this is the shape to expect from an Axum API with PostgreSQL and SeaORM:
my-rust-api/
Cargo.toml
.env.example
src/
main.rs
config.rs
error.rs
routes/
mod.rs
health.rs
users.rs
entities/
mod.rs
prelude.rs
user.rs
services/
mod.rs
users.rsUse routes/ for HTTP mechanics, services/ for application behavior, and entities/ for SeaORM's generated or maintained database model types.
Route and model examples
An Axum handler should usually accept extracted state and typed request data, then return a result that your shared error type can convert into a response:
use axum::{extract::State, Json};
use serde::{Deserialize, Serialize};
use crate::{error::AppError, services::users::UserService, AppState};
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
pub email: String,
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct UserResponse {
pub id: i32,
pub email: String,
pub name: String,
}
pub async fn create_user(
State(state): State<AppState>,
Json(input): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>, AppError> {
let user = UserService::new(state.db)
.create(input.email, input.name)
.await?;
Ok(Json(UserResponse {
id: user.id,
email: user.email,
name: user.name,
}))
}SeaORM keeps inserts explicit through active models:
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection};
use crate::entities::user;
pub async fn create_user(
db: &DatabaseConnection,
email: String,
name: String,
) -> Result<user::Model, sea_orm::DbErr> {
user::ActiveModel {
email: Set(email),
name: Set(name),
..Default::default()
}
.insert(db)
.await
}Compatibility notes
When to choose it
Choose Axum when performance, correctness, and explicit server code matter. It is a good fit for APIs, backend services, and systems where Rust's type system is part of the product strategy.
Tradeoffs
Rust usually asks for more upfront type modeling than TypeScript, Python, or Go. In return, you get strong compile-time checks and predictable runtime behavior.
SeaORM is productive when your team wants generated entity types and composable query builders. It can feel heavier than SQLx for query-heavy services where hand-written SQL is part of the design.
Testing and deployment notes
Start by testing handlers and services separately. Handler tests can build an Axum router with test state, while service tests can run against a disposable PostgreSQL database.
cargo testBefore deployment, make sure the runtime environment provides a PostgreSQL connection string and any migration step your project uses. For production, keep RUST_LOG explicit so tracing output is useful without being too noisy:
RUST_LOG=info cargo runTroubleshooting
- If the app starts but database calls fail, verify the generated
.env.examplekeys match the variables set in your shell or deployment platform. - If SeaORM types do not match the database, rerun the project's entity or migration workflow instead of manually editing generated code.
- If errors return as generic 500 responses, check the shared error type and its
IntoResponseimplementation. - If logs are missing request context, confirm the tracing subscriber is initialized before the router starts serving traffic.
Next steps
- Open the Stack Builder.
- Read the Rust ecosystem docs.
- Review the CLI create reference.