Skip to main content

Agent Skills Framework Extension

Actix-Web Patterns Skill

When to Use This Skill

Use this skill when implementing actix web patterns patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

High-performance async Rust web services with Actix-web framework.

Core Capabilities

  1. Application Structure - Modular Actix-web organization
  2. Handlers - Async request handling patterns
  3. Middleware - Request/response transformation
  4. Extractors - Type-safe request parsing
  5. Error Handling - Custom error responses

Application Structure

// src/main.rs
use actix_web::{web, App, HttpServer, middleware};
use sqlx::PgPool;

mod config;
mod handlers;
mod middleware as app_middleware;
mod models;
mod errors;

pub struct AppState {
pub db: PgPool,
pub config: config::Settings,
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Initialize logging
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

// Load configuration
let settings = config::Settings::from_env().expect("Failed to load config");

// Create database pool
let pool = PgPool::connect(&settings.database_url)
.await
.expect("Failed to create pool");

// Run migrations
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("Failed to run migrations");

let app_state = web::Data::new(AppState {
db: pool,
config: settings.clone(),
});

HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
// Middleware
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.wrap(app_middleware::cors::cors_config())
.wrap(app_middleware::security::SecurityHeaders)
// Routes
.configure(handlers::health::configure)
.configure(handlers::auth::configure)
.configure(handlers::users::configure)
.configure(handlers::api::configure)
})
.bind(("0.0.0.0", settings.port))?
.run()
.await
}

Handler Patterns

// src/handlers/users.rs
use actix_web::{web, HttpResponse, Result};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;

use crate::errors::AppError;
use crate::models::user::User;

pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/users")
.route("", web::get().to(list_users))
.route("", web::post().to(create_user))
.route("/{id}", web::get().to(get_user))
.route("/{id}", web::put().to(update_user))
.route("/{id}", web::delete().to(delete_user)),
);
}

#[derive(Debug, Deserialize)]
pub struct ListQuery {
#[serde(default = "default_limit")]
limit: i64,
#[serde(default)]
offset: i64,
}

fn default_limit() -> i64 { 20 }

#[derive(Debug, Serialize)]
pub struct ListResponse<T> {
data: Vec<T>,
total: i64,
limit: i64,
offset: i64,
}

async fn list_users(
pool: web::Data<PgPool>,
query: web::Query<ListQuery>,
) -> Result<HttpResponse, AppError> {
let users = sqlx::query_as!(
User,
r#"
SELECT id, email, name, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
"#,
query.limit,
query.offset,
)
.fetch_all(pool.get_ref())
.await?;

let total = sqlx::query_scalar!("SELECT COUNT(*) FROM users")
.fetch_one(pool.get_ref())
.await?
.unwrap_or(0);

Ok(HttpResponse::Ok().json(ListResponse {
data: users,
total,
limit: query.limit,
offset: query.offset,
}))
}

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
email: String,
name: String,
password: String,
}

async fn create_user(
pool: web::Data<PgPool>,
body: web::Json<CreateUserRequest>,
) -> Result<HttpResponse, AppError> {
// Validate email uniqueness
let existing = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)",
&body.email
)
.fetch_one(pool.get_ref())
.await?;

if existing.unwrap_or(false) {
return Err(AppError::Conflict("Email already exists".into()));
}

// Hash password
let password_hash = bcrypt::hash(&body.password, 12)
.map_err(|_| AppError::Internal("Failed to hash password".into()))?;

let user = sqlx::query_as!(
User,
r#"
INSERT INTO users (email, name, password_hash)
VALUES ($1, $2, $3)
RETURNING id, email, name, created_at, updated_at
"#,
&body.email,
&body.name,
&password_hash,
)
.fetch_one(pool.get_ref())
.await?;

Ok(HttpResponse::Created().json(user))
}

async fn get_user(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
) -> Result<HttpResponse, AppError> {
let id = path.into_inner();

let user = sqlx::query_as!(
User,
"SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1",
id
)
.fetch_optional(pool.get_ref())
.await?
.ok_or_else(|| AppError::NotFound("User not found".into()))?;

Ok(HttpResponse::Ok().json(user))
}

Middleware Patterns

// src/middleware/security.rs
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpResponse,
};
use futures::future::{ok, Ready};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

pub struct SecurityHeaders;

impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = SecurityHeadersMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;

fn new_transform(&self, service: S) -> Self::Future {
ok(SecurityHeadersMiddleware { service })
}
}

pub struct SecurityHeadersMiddleware<S> {
service: S,
}

impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

forward_ready!(service);

fn call(&self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req);

Box::pin(async move {
let mut res = fut.await?;

let headers = res.headers_mut();
headers.insert(
"X-Content-Type-Options".parse().unwrap(),
"nosniff".parse().unwrap(),
);
headers.insert(
"X-Frame-Options".parse().unwrap(),
"DENY".parse().unwrap(),
);
headers.insert(
"X-XSS-Protection".parse().unwrap(),
"1; mode=block".parse().unwrap(),
);
headers.insert(
"Strict-Transport-Security".parse().unwrap(),
"max-age=31536000; includeSubDomains".parse().unwrap(),
);

Ok(res)
})
}
}

// src/middleware/auth.rs
use actix_web::{dev::ServiceRequest, Error, HttpMessage};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use jsonwebtoken::{decode, DecodingKey, Validation};

use crate::models::claims::Claims;

pub async fn jwt_validator(
req: ServiceRequest,
credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let token = credentials.token();

let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let key = DecodingKey::from_secret(secret.as_bytes());

match decode::<Claims>(token, &key, &Validation::default()) {
Ok(token_data) => {
req.extensions_mut().insert(token_data.claims);
Ok(req)
}
Err(e) => {
log::warn!("JWT validation failed: {:?}", e);
Err((actix_web::error::ErrorUnauthorized("Invalid token"), req))
}
}
}

Request Extractors

// src/extractors/auth.rs
use actix_web::{dev::Payload, FromRequest, HttpRequest, Error};
use futures::future::{ok, Ready};

use crate::models::claims::Claims;
use crate::errors::AppError;

/// Authenticated user extractor
pub struct AuthUser(pub Claims);

impl FromRequest for AuthUser {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;

fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
match req.extensions().get::<Claims>() {
Some(claims) => ok(AuthUser(claims.clone())),
None => ok(Err(AppError::Unauthorized("Not authenticated".into()).into())?),
}
}
}

// Usage in handlers
async fn get_profile(
auth: AuthUser,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
auth.0.sub
)
.fetch_one(pool.get_ref())
.await?;

Ok(HttpResponse::Ok().json(user))
}

// src/extractors/pagination.rs
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Pagination {
#[serde(default = "default_page")]
pub page: u32,
#[serde(default = "default_per_page")]
pub per_page: u32,
}

fn default_page() -> u32 { 1 }
fn default_per_page() -> u32 { 20 }

impl Pagination {
pub fn offset(&self) -> i64 {
((self.page.saturating_sub(1)) * self.per_page) as i64
}

pub fn limit(&self) -> i64 {
self.per_page.min(100) as i64
}
}

Error Handling

// src/errors.rs
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use serde::Serialize;
use std::fmt;

#[derive(Debug)]
pub enum AppError {
NotFound(String),
BadRequest(String),
Unauthorized(String),
Forbidden(String),
Conflict(String),
Internal(String),
Database(sqlx::Error),
Validation(validator::ValidationErrors),
}

#[derive(Serialize)]
struct ErrorResponse {
error: ErrorDetail,
}

#[derive(Serialize)]
struct ErrorDetail {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<serde_json::Value>,
}

impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
AppError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
AppError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
AppError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
AppError::Conflict(msg) => write!(f, "Conflict: {}", msg),
AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
AppError::Database(e) => write!(f, "Database error: {}", e),
AppError::Validation(e) => write!(f, "Validation error: {:?}", e),
}
}
}

impl ResponseError for AppError {
fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound(_) => StatusCode::NOT_FOUND,
AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
AppError::Forbidden(_) => StatusCode::FORBIDDEN,
AppError::Conflict(_) => StatusCode::CONFLICT,
AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
}
}

fn error_response(&self) -> HttpResponse {
let (code, message, details) = match self {
AppError::NotFound(msg) => ("NOT_FOUND", msg.clone(), None),
AppError::BadRequest(msg) => ("BAD_REQUEST", msg.clone(), None),
AppError::Unauthorized(msg) => ("UNAUTHORIZED", msg.clone(), None),
AppError::Forbidden(msg) => ("FORBIDDEN", msg.clone(), None),
AppError::Conflict(msg) => ("CONFLICT", msg.clone(), None),
AppError::Internal(msg) => ("INTERNAL_ERROR", msg.clone(), None),
AppError::Database(_) => ("DATABASE_ERROR", "Database operation failed".into(), None),
AppError::Validation(e) => (
"VALIDATION_ERROR",
"Validation failed".into(),
Some(serde_json::to_value(e).unwrap_or_default()),
),
};

HttpResponse::build(self.status_code()).json(ErrorResponse {
error: ErrorDetail {
code: code.into(),
message,
details,
},
})
}
}

impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
log::error!("Database error: {:?}", err);
AppError::Database(err)
}
}

impl From<validator::ValidationErrors> for AppError {
fn from(err: validator::ValidationErrors) -> Self {
AppError::Validation(err)
}
}

WebSocket Integration

// src/handlers/websocket.rs
use actix::{Actor, StreamHandler, AsyncContext, Handler, Message};
use actix_web::{web, HttpRequest, HttpResponse, Error};
use actix_web_actors::ws;
use serde::{Deserialize, Serialize};

pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.route("/ws", web::get().to(ws_handler));
}

async fn ws_handler(
req: HttpRequest,
stream: web::Payload,
) -> Result<HttpResponse, Error> {
ws::start(WebSocketSession::new(), &req, stream)
}

pub struct WebSocketSession {
heartbeat: std::time::Instant,
}

impl WebSocketSession {
pub fn new() -> Self {
Self {
heartbeat: std::time::Instant::now(),
}
}

fn heartbeat(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(std::time::Duration::from_secs(5), |act, ctx| {
if std::time::Instant::now().duration_since(act.heartbeat) > std::time::Duration::from_secs(30) {
ctx.stop();
return;
}
ctx.ping(b"");
});
}
}

impl Actor for WebSocketSession {
type Context = ws::WebsocketContext<Self>;

fn started(&mut self, ctx: &mut Self::Context) {
self.heartbeat(ctx);
}
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
enum WsMessage {
Subscribe { channel: String },
Unsubscribe { channel: String },
Message { channel: String, data: serde_json::Value },
}

impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketSession {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Ping(msg)) => {
self.heartbeat = std::time::Instant::now();
ctx.pong(&msg);
}
Ok(ws::Message::Pong(_)) => {
self.heartbeat = std::time::Instant::now();
}
Ok(ws::Message::Text(text)) => {
if let Ok(ws_msg) = serde_json::from_str::<WsMessage>(&text) {
match ws_msg {
WsMessage::Subscribe { channel } => {
log::info!("Subscribed to channel: {}", channel);
}
WsMessage::Unsubscribe { channel } => {
log::info!("Unsubscribed from channel: {}", channel);
}
WsMessage::Message { channel, data } => {
log::info!("Message to {}: {:?}", channel, data);
}
}
}
}
Ok(ws::Message::Close(reason)) => {
ctx.close(reason);
ctx.stop();
}
_ => (),
}
}
}

Usage Examples

Create Actix-web API

Apply actix-web-patterns skill to scaffold a new Actix-web REST API with authentication

Add Middleware

Apply actix-web-patterns skill to implement rate limiting middleware for API endpoints

WebSocket Server

Apply actix-web-patterns skill to add real-time WebSocket support for live updates

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: actix-web-patterns

Completed:
- [x] Actix-web application structured with modular handlers
- [x] Type-safe request extractors implemented (Path, Query, Json)
- [x] Custom middleware added (Security headers, CORS, Auth)
- [x] Error handling with custom AppError and HTTP status mapping
- [x] Database integration with SQLx connection pooling
- [x] WebSocket endpoint functional (if applicable)

Outputs:
- src/main.rs (application entry point with server config)
- src/handlers/*.rs (modular route handlers)
- src/middleware/*.rs (security, auth middleware)
- src/extractors/*.rs (custom request extractors)
- src/errors.rs (centralized error handling)
- Cargo.toml (dependencies: actix-web, sqlx, serde)

Verification:
- Server starts on configured port: ✓
- Health check endpoint returns 200: ✓
- Middleware applied in correct order: ✓
- Database migrations run successfully: ✓
- Error responses follow JSON format: ✓

Completion Checklist

Before marking this skill as complete, verify:

  • cargo build compiles without errors
  • cargo test passes all tests
  • Server starts and binds to configured port (0.0.0.0:8080)
  • Health check endpoint accessible: curl http://localhost:8080/health
  • Database connection pool initialized successfully
  • Middleware applied in order: Logger → Compress → CORS → Security → Auth
  • Error responses return proper JSON format with status codes
  • CORS configuration allows expected origins
  • Security headers present in responses (X-Frame-Options, CSP)
  • JWT validation works for protected endpoints (if auth implemented)

Failure Indicators

This skill has FAILED if:

  • ❌ Compilation errors due to type mismatches or missing traits
  • ❌ Server panics on startup (database connection, port binding)
  • ❌ Middleware not applied or applied in wrong order
  • ❌ Error responses return HTML instead of JSON
  • ❌ CORS blocks legitimate requests (misconfigured origins)
  • ❌ Database queries fail with connection pool exhausted
  • ❌ Authentication middleware bypassed for protected routes
  • ❌ WebSocket connections immediately disconnect (if implemented)

When NOT to Use

Do NOT use actix-web-patterns when:

  • Building simple CLI tools or scripts (no web server needed)
  • Prototyping where development speed > performance (use Python/Flask instead)
  • Team lacks Rust expertise (steep learning curve)
  • Project requires extensive async/await (Tokio-based frameworks like Axum may be simpler)
  • Lightweight HTTP server sufficient (use hyper directly)
  • GraphQL API primary requirement (consider async-graphql examples instead)
  • Windows-only deployment with limited async runtime support

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Blocking I/O in handlersBlocks worker threads, poor concurrencyUse async/await with tokio-based clients
Shared mutable state without locksData races, undefined behaviorUse web::Data<Arc<Mutex<T>>> or RwLock
Middleware order confusionAuth bypassed, CORS issuesApply in order: Logger → Compress → CORS → Auth
Raw String errorsNo HTTP status mapping, unclear errorsUse custom AppError enum with ResponseError trait
Database connections per requestConnection exhaustionUse connection pool in app state (PgPool)
Hardcoded configurationDeployment issues, secrets exposedLoad config from environment variables
No request validationSQL injection, XSS vulnerabilitiesUse Serde deserialize with validation crate

Principles

This skill embodies:

  • #5 Eliminate Ambiguity - Type-safe extractors prevent runtime parsing errors
  • #6 Clear, Understandable, Explainable - Modular handler structure separates concerns
  • #8 No Assumptions - Explicit error handling for all failure modes
  • #9 Quality Over Speed - Async I/O and connection pooling for production reliability
  • #12 Separation of Concerns - Handlers, middleware, errors, extractors in separate modules

Full Principles: CODITECT-STANDARD-AUTOMATION.md

Integration Points

  • rust-backend-patterns - General Rust backend patterns
  • authentication-authorization - Auth middleware integration
  • error-handling-resilience - Error handling patterns