Actix Web
SkillDocs & knowledgeActix 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Actix Web skill
What this skill tells your AI
The instructions your AI receives, as published by terminalskills/skills in skills/actix-web/SKILL.md and read by ahel’s review.
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.
Installation
# 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"
Project Structure
# 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 types
Main and Server Setup
// 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
}
Route Configuration
// 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))
)
);
}
Models
// 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>,
}
Handlers with Extractors
// 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))
}
Error Handling
// 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) }
}
Middleware
// 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")),
}
}
Testing
// 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);
}
Key Patterns
- Use extractors (
web::Json,web::Path,web::Query,web::Data) for type-safe request parsing - Implement
ResponseErroron custom error types for automatic HTTP error responses - Use
web::Datafor shared application state (DB pool, config) — it's cheaply cloneable - Use
sqlxwith compile-time checked queries (sqlx::query!) for production code - Use
configurefunctions to modularize route registration - Use
#[actix_web::test]for async integration tests withtest::init_service
Signals
- GitHub stars
- 148
- Forks
- 18
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
actix-web-terminalskills- Source
- github.com/terminalskills/skills