Open Source · MIT License

Production-Ready
Rust API Starter

A clean-architecture foundation built on Axum 0.8 and Diesel 2.3. Authentication, file uploads, OpenAPI docs and structured logging — all wired up from day one.

Axum 0.8 Diesel 2.3 SQLite / PostgreSQL JWT + Argon2 utoipa OpenAPI Tokio async Tower middleware tracing

Everything you need, nothing you don't

Opinionated defaults with strict layering so the team stays consistent from the first commit.

Axum 0.8

Ergonomic, macro-free routing from the Tokio team with Tower middleware composability.

🗃

Diesel ORM

Type-safe queries with compile-time schema checking. SQLite in dev, PostgreSQL in production, r2d2 pooling throughout.

🔐

JWT Auth

Access + refresh token rotation, Argon2 password hashing, and a self-contained AuthUser extractor — no separate middleware needed.

🏗

Clean Architecture

Strict Controller → Service → Repository layering enforced by convention. No HTTP types leak into the service layer.

📑

Auto OpenAPI

utoipa generates a full OpenAPI spec from #[utoipa::path] annotations inline with each handler. Swagger UI at /spec (dev only).

📂

File Uploads

Streaming multipart uploads with MIME allowlist validation, filename sanitisation, and a reusable FormData extractor.

📋

Structured Logging

Environment-aware tracing setup: pretty console in dev, JSON to file + stdout in production.

🔄

Graceful Shutdown

Tower's signal handler + connection drain ensures in-flight requests complete before the process exits.

🧪

Integration Tests

TestApp::spawn() spins up an isolated SQLite database per test, hitting real HTTP endpoints via reqwest.


Strict three-layer separation

Each layer has one job. Dependencies flow in one direction — outer layers call inner ones, never the reverse.

Controller

HTTP Transport

Parses requests via extractors, calls the service, maps errors with HttpError::from_service_error(), returns HttpResponse. No business logic.

↓ calls
Service

Business Logic

Validates rules, orchestrates repositories. Returns anyhow::Result<T>. Zero axum or StatusCode imports allowed.

↓ calls
Repository

Data Access

Only Diesel queries — no raw SQL. Returns anyhow::Result<T> with short uppercase error codes like "NOT_FOUND".

Error flow: service returns bail!("NOT_FOUND") → controller maps to 404 → client receives {"success":false,"message":"NOT_FOUND"}. New error codes are registered in one place: src/services/http_error.rs.

// repository.rs — data access only
pub async fn find_by_id(db: &DBSqlite, uid: String)
  -> anyhow::Result<User>
{
  db.execute(|conn| {
    users::table.find(&uid).first(conn).optional()
  }).await?
  .ok_or_else(|| anyhow!("NOT_FOUND"))
}

// service.rs — business logic only
pub async fn get_user(db: &DBSqlite, uid: String)
  -> anyhow::Result<User>
{
  repository::find_by_id(db, uid).await
}

// controller.rs — HTTP only
pub async fn get_by_id(
  State(state): State<Arc<AppState>>,
  PathParam(uid): PathParam<String>,
) -> Result<impl IntoResponse, HttpError> {
  let user = service::get_user(&state.db, uid)
    .await
    .map_err(HttpError::from_service_error)?;
  Ok(HttpResponse::ok(user, "OK"))
}

Vertical-slice modules

Features live in self-contained directories under src/modules/. Each module owns its model, repository, service, and controller.

src/
├── main.rs              # Entry point
├── lib.rs               # Module declarations
├── config.rs            # Env loading, logging init
├── server.rs            # AppServer, middleware, shutdown
├── models/              # Shared domain models
│   └── environment.rs   # Environment, AppState
├── modules/             # Feature modules
│   ├── doc.rs           # ApiDoc aggregator
│   ├── health/          # /health/live, /health/ready
│   ├── auth/            # register, login, refresh
│   ├── user/            # users CRUD
│   └── attachment/      # file uploads
├── extractors/          # Custom Axum extractors
│   ├── auth.rs          # AuthUser — JWT validation
│   ├── body.rs          # BodyJson with validation
│   ├── path.rs          # PathParam — typed path params
│   └── formdata.rs      # Multipart + file validation
├── services/            # Infrastructure
│   ├── http_error.rs    # HttpError + error mapper
│   ├── http_response.rs # HttpResponse wrapper
│   └── sqlite.rs        # DBSqlite pool wrapper
├── schemas/
│   └── table.rs         # Diesel table! macros
└── utils/
    ├── token.rs         # JWT sign/verify
    ├── encrypt.rs       # Argon2 hashing
    ├── files.rs         # Upload/delete helpers
    └── generator.rs     # Snowflake ID

Each feature module follows the same internal layout:

auth/
├── mod.rs        # Routes — AuthRoutes::build()
├── model.rs      # RegisterRequest, LoginRequest…
├── repository.rs # Diesel queries
├── service.rs    # Business logic
└── controller.rs # HTTP handlers + utoipa docs

Adding a new module means creating this structure and registering it in src/modules/mod.rs. No framework-level changes needed.

Database changes follow standard Diesel migration workflow via ./run.sh db:migration:create <name> — never run diesel or cargo directly, as run.sh handles env loading.


Available endpoints

Routes follow REST conventions. Resources use plural nouns; action routes use verbs after the prefix.

Method Path Handler Auth Description
GET /health/live liveness Kubernetes liveness probe
GET /health/ready readiness Kubernetes readiness probe
POST /auth/register register Create account, returns tokens
POST /auth/login login Email + password login
POST /auth/refresh refresh Rotate refresh token, issue new access token
GET /users/me get_me JWT Current user profile
GET /users list JWT Paginated user list
GET /users/{id} get_by_id JWT Single user by ID
POST /attachments/upload upload JWT Multipart file upload
GET /attachments list JWT Paginated attachment list
GET /attachments/{id} get_by_id JWT Attachment metadata
PATCH /attachments/{id} update JWT Update attachment metadata
DELETE /attachments/{id} delete JWT Delete attachment and file

Naming at a glance

Consistent naming makes the codebase predictable and searchable.

Type names

Category Pattern Example
DB entity {Entity} User
Insertable New{Entity} NewUser
Response DTO {Entity}Response UserResponse
Request DTO {Action}{Entity}Request RegisterRequest
Query params {Entity}Query UserQuery

Handler names

Operation Name
List resources list
Get single get_by_id
Current user get_me
Create create
Update update
Delete delete
Auth actions register / login / refresh