Template do pack (backend/09-rust-axum.md). Orienta o agente em APIs, servicos e arquitetura backend alinhado a esse contexto.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add artubss/SKILLS-CLAUDE-CODE --skill tpl-backend-rust-axum --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Tpl Backend Rust Axum?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/artubss-tpl-backend-rust-axum)More formats (shields.io, HTML) on the badges page.
---
name: tpl-backend-rust-axum
description: Template do pack (backend/09-rust-axum.md). Orienta o agente em APIs, servicos e arquitetura backend alinhado a esse contexto.
metadata:
version: 1.0.0
source_template: backend/09-rust-axum.md
generated_by: install_pack_templates_as_claude_skills
---
# PROJECT: Rust Axum REST API
Skill gerado a partir do pack `templates-claude-code`. Arquivo de origem: `backend/09-rust-axum.md`. Use como baseline e adapte ao projeto antes de mudancas grandes.
## Conteudo do template
## STACK
- **Language:** Rust (edition 2021, stable)
- **Framework:** Axum 0.7
- **Database:** PostgreSQL via SQLx 0.7
- **Middleware:** Tower + tower-http
- **Serialization:** serde + serde_json
- **Error handling:** thiserror + anyhow
- **Tracing:** tracing + tracing-subscriber
- **Testing:** cargo test + sqlx test macros
---
## PROJECT STRUCTURE
```
src/
├── main.rs # Server bootstrap, router assembly
├── config.rs # Config struct from env vars
├── state.rs # AppState definition
├── error.rs # AppError enum (thiserror)
├── db/
│ └── mod.rs # PgPool creation + migrations
├── routes/
│ ├── mod.rs # Router assembly
│ ├── users.rs
│ └── posts.rs
├── handlers/
│ ├── users.rs
│ └── posts.rs
├── models/
│ ├── user.rs # DB model + request/response types
│ └── post.rs
└── middleware/
├── auth.rs # JWT extractor
└── request_id.rs
migrations/
└── 001_init.sql
tests/
└── integration/
└── users_test.rs
```
---
## ARCHITECTURE RULES
1. **`AppState` is cloned via `Arc`** — never use `Mutex` for read-heavy shared state; use `RwLock` if writes are needed.
2. **Extractors validate input** — use custom `JsonExtractor<T>` that returns `AppError` on invalid JSON.
3. **`AppError` implements `IntoResponse`** — handlers return `Result<impl IntoResponse, AppError>` only.
4. **SQLx compile-time checked macros** — prefer `sqlx::query_as!` over raw query strings.
5. **Tracing spans on every handler** — `#[tracing::instrument]` on all public handler functions.
6. **No `unwrap()`/`expect()` in handlers** — propagate errors through `?` operator.
7. **Database migrations run at startup** — `sqlx::migrate!().run(&pool).await?` in `main`.
---
## ERROR TYPES (thiserror)
```rust
// src/error.rs
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Unprocessable entity: {0}")]
UnprocessableEntity(String),
#[error("Internal error")]
Internal(#[from] anyhow::Error),
#[error("Database error")]
Database(#[from] sqlx::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
Self::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
Self::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
Self::UnprocessableEntity(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg.clone()),
Self::Internal(_) | Self::Database(_) => {
tracing::error!(error = %self);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into())
}
};
(status, Json(json!({ "error": message }))).into_response()
}
}
```
---
## STATE MANAGEMENT WITH ARC
```rust
// src/state.rs
use sqlx::PgPool;
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
pub config: Arc<crate::config::Config>,
}
// src/config.rs
#[derive(Debug, Clone)]
pub struct Config {
pub database_url: String,
pub jwt_secret: String,
pub port: u16,
}
impl Config {
pub fn from_env() -> anyhow::Result<Self> {
Ok(Self {
database_url: std::env::var("DATABASE_URL")?,
jwt_secret: std::env::var("JWT_SECRET")?,
port: std::env::var("PORT").unwrap_or("3000".into()).parse()?,
})
}
}
// src/main.rs
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let config = Arc::new(Config::from_env()?);
let pool = PgPool::connect(&config.database_url).await?;
sqlx::migrate!("./migrations").run(&pool).await?;
let state = AppState { db: pool, config };
let router = routes::create_router(state);
let addr = format!("0.0.0.0:{}", config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Listening on {addr}");
axum::serve(listener, router).await?;
Ok(())
}
```
---
## EXTRACTOR PATTERN + HANDLER
```rust
// src/models/user.rs
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Serialize, sqlx::FromRow)]
pub struct User {
pub id: Uuid,
pub email: String,
pub name: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize, validator::Validate)]
pub struct CreateUserRequest {
#[validate(email)]
pub email: String,
#[validate(length(min = 2, max = 100))]
pub name: String,
}
// src/handlers/users.rs
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use uuid::Uuid;
#[tracing::instrument(skip(state))]
pub async fn create_user(
State(state): State<AppState>,
Json(body): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<User>), AppError> {
let user = sqlx::query_as!(
User,
"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
body.email,
body.name
)
.fetch_one(&state.db)
.await?;
Ok((StatusCode::CREATED, Json(user)))
}
#[tracing::instrument(skip(state))]
pub async fn get_user(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<User>, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(&state.db)
.await?
.ok_or_else(|| AppError::NotFound(format!("User {id} not found")))?;
Ok(Json(user))
}
// src/routes/mod.rs
use axum::{routing::get, Router};
pub fn create_router(state: AppState) -> Router {
Router::new()
.route("/api/users", axum::routing::post(handlers::users::create_user))
.route("/api/users/:id", get(handlers::users::get_user))
.layer(tower_http::trace::TraceLayer::new_for_http())
.with_state(state)
}
```
---
## ROUTING TABLE
| Method | Path | Auth | Action |
|--------|------|------|--------|
| POST | /api/users | No | Create user |
| GET | /api/users | Yes | List users (paginated) |
| GET | /api/users/:id | Yes | Get user by UUID |
| PUT | /api/users/:id | Yes (owner) | Update user |
| DELETE | /api/users/:id | Yes (admin) | Delete user |
| POST | /api/auth/login | No | Authenticate, return JWT |
| GET | /api/posts | No | List public posts |
| POST | /api/posts | Yes | Create post |
| GET | /api/posts/:id | No | Get post |
| GET | /health | No | Liveness probe (200 OK) |
---
## QUALITY GATES
- [ ] `cargo check` — zero warnings
- [ ] `cargo clippy -- -D warnings` — clean
- [ ] `cargo test` — all tests pass
- [ ] No `unwrap()` or `expect()` outside of `main` or test code
- [ ] All handlers instrument with `#[tracing::instrument]`
- [ ] `AppError::Internal` logs full error; client never sees stack trace
- [ ] SQLx macro queries verified against DB schema (`cargo sqlx prepare`)
- [ ] Migration files committed and idempotent
- [ ] `cargo audit` — no known vulnerabilities
---
## FORBIDDEN
- ❌ `panic!()` / `unwrap()` in handler code paths
- ❌ `.clone()` on large data structures inside hot paths
- ❌ Blocking I/O (`std::fs`, `std::thread::sleep`) inside async handlers — use `tokio::fs`, `tokio::time`
- ❌ `Mutex<PgPool>` — SQLx pool is already `Clone + Send + Sync`
- ❌ Returning raw `sqlx::Error` to the client
- ❌ Hardcoded SQL table names as string literals outside of `query!` macros
- ❌ Skipping `sqlx::migrate!()` at startup
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!