Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Actix Web is a powerful, high-performance web framework for Rust. It provides async handlers, type-safe extractors, middleware, and integrates with the Rust ecosystem for building fast, reliable, and memory-safe web services.
.claude/skills/terminalskills-actix-web/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-19 | ✓→✗ | ▼ Worse | 75% | 0% |
| case-10 | ✓→✓ | = Same ✓ | 64% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 33% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 41% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 129% | 0% |
Actix Web is one of the fastest web frameworks available. It uses Rust's type system for compile-time safety, async/await for concurrency, and extractors for ergonomic request handling.
toml# Cargo.toml — dependencies [dependencies] actix-web = "4" actix-rt = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "macros"] } tokio = { version = "1", features = ["full"] } env_logger = "0.11"
# Recommended Actix Web project layout
src/
├── main.rs # Entry point and server config
├── config.rs # Configuration
├── routes/
│ ├── mod.rs # Route registration
│ ├── articles.rs # Article handlers
│ └── health.rs # Health check
├── models/
│ └── article.rs # Data structures
├── db/
│ └── article.rs # Database queries
├── middleware/
│ └── auth.rs # Auth middleware
└── errors.rs # Error typesrust// src/main.rs — application entry point use actix_web::{web, App, HttpServer, middleware::Logger}; use sqlx::postgres::PgPoolOptions; mod routes; mod models; mod db; mod errors; #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://localhost/mydb".to_string()); let pool = PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .expect("Failed to create pool"); HttpServer::new(move || { App::new() .wrap(Logger::default()) .app_data(web::Data::new(pool.clone())) .configure(routes::configure) }) .bind("0.0.0.0:8080")? .run() .await }
rust// src/routes/mod.rs — centralized route registration use actix_web::web; mod articles; mod health; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/api") .route("/health", web::get().to(health::check)) .service( web::scope("/articles") .route("", web::get().to(articles::list)) .route("", web::post().to(articles::create)) .route("/{id}", web::get().to(articles::get)) .route("/{id}", web::delete().to(articles::delete)) ) ); }
rust// src/models/article.rs — data models with serde use serde::{Deserialize, Serialize}; use sqlx::FromRow; #[derive(Debug, Serialize, FromRow)] pub struct Article { pub id: i32, pub title: String, pub body: String, pub published: bool, pub created_at: chrono::NaiveDateTime, } #[derive(Debug, Deserialize)] pub struct CreateArticle { pub title: String, pub body: String, } #[derive(Debug, Deserialize)] pub struct ListParams { pub page: Option<u32>, pub limit: Option<u32>, }
rust// src/routes/articles.rs — request handlers use actix_web::{web, HttpResponse}; use sqlx::PgPool; use crate::models::article::{Article, CreateArticle, ListParams}; use crate::errors::AppError; pub async fn list( pool: web::Data<PgPool>, query: web::Query<ListParams>, ) -> Result<HttpResponse, AppError> { let limit = query.limit.unwrap_or(20).min(100) as i64; let offset = ((query.page.unwrap_or(1) - 1) * limit as u32) as i64; let articles = sqlx::query_as::<_, Article>( "SELECT * FROM articles WHERE published = true ORDER BY created_at DESC LIMIT $1 OFFSET $2" ) .bind(limit) .bind(offset) .fetch_all(pool.get_ref()) .await?; Ok(HttpResponse::Ok().json(articles)) } pub async fn create( pool: web::Data<PgPool>, body: web::Json<CreateArticle>, ) -> Result<HttpResponse, AppError> { let article = sqlx::query_as::<_, Article>( "INSERT INTO articles (title, body) VALUES ($1, $2) RETURNING *" ) .bind(&body.title) .bind(&body.body) .fetch_one(pool.get_ref()) .await?; Ok(HttpResponse::Created().json(article)) } pub async fn get( pool: web::Data<PgPool>, path: web::Path<i32>, ) -> Result<HttpResponse, AppError> { let id = path.into_inner(); let article = sqlx::query_as::<_, Article>("SELECT * FROM articles WHERE id = $1") .bind(id) .fetch_optional(pool.get_ref()) .await? .ok_or(AppError::NotFound)?; Ok(HttpResponse::Ok().json(article)) }
rust// src/errors.rs — custom error types use actix_web::{HttpResponse, ResponseError}; use std::fmt; #[derive(Debug)] pub enum AppError { NotFound, Internal(String), Database(sqlx::Error), } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppError::NotFound => write!(f, "Not found"), AppError::Internal(msg) => write!(f, "{}", msg), AppError::Database(e) => write!(f, "Database error: {}", e), } } } impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { match self { AppError::NotFound => HttpResponse::NotFound().json(serde_json::json!({"error": "not found"})), _ => HttpResponse::InternalServerError().json(serde_json::json!({"error": "internal error"})), } } } impl From<sqlx::Error> for AppError { fn from(e: sqlx::Error) -> Self { AppError::Database(e) } }
rust// src/middleware/auth.rs — simple auth middleware use actix_web::{dev::ServiceRequest, Error, HttpMessage}; use actix_web::error::ErrorUnauthorized; pub async fn validate_token(req: &ServiceRequest) -> Result<(), Error> { let token = req.headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")); match token { Some(t) => { let user_id = verify_jwt(t).map_err(|_| ErrorUnauthorized("invalid token"))?; req.extensions_mut().insert(user_id); Ok(()) } None => Err(ErrorUnauthorized("missing token")), } }
rust// tests/articles_test.rs — integration test use actix_web::{test, App, web}; #[actix_web::test] async fn test_list_articles() { let pool = setup_test_db().await; let app = test::init_service( App::new() .app_data(web::Data::new(pool)) .configure(routes::configure) ).await; let req = test::TestRequest::get().uri("/api/articles").to_request(); let resp = test::call_service(&app, req).await; assert_eq!(resp.status(), 200); }
web::Json, web::Path, web::Query, web::Data) for type-safe request parsingResponseError on custom error types for automatic HTTP error responsesweb::Data for shared application state (DB pool, config) — it's cheaply cloneablesqlx with compile-time checked queries (sqlx::query!) for production codeconfigure functions to modularize route registration#[actix_web::test] for async integration tests with test::init_service| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | pass→pass | 9,833 | 6,535 | -34% | 1 | 1 | 0% | 2,257 | 3,698 | +64% | 0 | 0 | — |
case-01 | pass→pass | 12,427 | 8,809 | -29% | 1 | 1 | 0% | 2,799 | 3,720 | +33% | 0 | 0 | — |
case-02 | fail→fail | 14,659 | 13,401 | -9% | 1 | 1 | 0% | 3,381 | 4,890 | +45% | 0 | 0 | — |
case-03 | fail→fail | 27,418 | 10,152 | -63% | 1 | 1 | 0% | 3,902 | 4,445 | +14% | 0 | 0 | — |
case-04 | pass→pass | 11,978 | 7,429 | -38% | 1 | 1 | 0% | 2,641 | 3,712 | +41% | 0 | 0 | — |
case-05 | pass→pass | 7,767 | 6,017 | -23% | 1 | 1 | 0% | 1,491 | 3,411 | +129% | 0 | 0 | — |
case-06 | pass→pass | 10,384 | 7,839 | -25% | 1 | 1 | 0% | 2,390 | 3,941 | +65% | 0 | 0 | — |
case-07 | pass→pass | 10,460 | 5,887 | -44% | 1 | 1 | 0% | 1,943 | 3,281 | +69% | 0 | 0 | — |
case-08 | pass→pass | 5,922 | 5,799 | -2% | 1 | 1 | 0% | 1,314 | 3,411 | +160% | 0 | 0 | — |
case-09 | pass→pass | 9,272 | 4,251 | -54% | 1 | 1 | 0% | 2,058 | 3,091 | +50% | 0 | 0 | — |
case-11 | pass→pass | 10,242 | 7,555 | -26% | 1 | 1 | 0% | 2,281 | 3,867 | +70% | 0 | 0 | — |
case-12 | fail→fail | 10,656 | 6,729 | -37% | 1 | 1 | 0% | 2,241 | 3,531 | +58% | 0 | 0 | — |
case-13 | pass→pass | 15,030 | 12,811 | -15% | 1 | 1 | 0% | 2,979 | 4,863 | +63% | 0 | 0 | — |
case-14 | pass→pass | 6,690 | 4,718 | -29% | 1 | 1 | 0% | 1,508 | 3,079 | +104% | 0 | 0 | — |
case-15 | pass→pass | 10,064 | 6,241 | -38% | 1 | 1 | 0% | 1,910 | 3,473 | +82% | 0 | 0 | — |
case-16 | pass→pass | 8,088 | 5,743 | -29% | 1 | 1 | 0% | 1,629 | 3,370 | +107% | 0 | 0 | — |
case-17 | fail→fail | 8,216 | 4,992 | -39% | 1 | 1 | 0% | 1,449 | 3,066 | +112% | 0 | 0 | — |
case-18 | pass→pass | 12,224 | 7,607 | -38% | 1 | 1 | 0% | 2,329 | 3,509 | +51% | 0 | 0 | — |
case-19 | pass→fail | 10,723 | 7,760 | -28% | 1 | 1 | 0% | 2,111 | 3,703 | +75% | 0 | 0 | — |
case-20 | pass→pass | 8,115 | 7,658 | -6% | 1 | 1 | 0% | 1,574 | 3,756 | +139% | 0 | 0 | — |
case-21 | pass→pass | 10,857 | 8,601 | -21% | 1 | 1 | 0% | 2,030 | 3,974 | +96% | 0 | 0 | — |
case-22 | pass→pass | 7,842 | 5,997 | -24% | 1 | 1 | 0% | 1,531 | 3,370 | +120% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of -5 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Other measured skills in the registry, with their headline benchmark lift.