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.
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.
HTTP Transport
Parses requests via extractors, calls the service, maps errors
with HttpError::from_service_error(), returns
HttpResponse. No business logic.
Business Logic
Validates rules, orchestrates repositories. Returns
anyhow::Result<T>. Zero
axum or StatusCode imports allowed.
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
|
Detailed guides
Everything you need to contribute confidently lives in the
guide/ directory.