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
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
High-performance async Rust web services with Actix-web framework.
Core Capabilities
- Application Structure - Modular Actix-web organization
- Handlers - Async request handling patterns
- Middleware - Request/response transformation
- Extractors - Type-safe request parsing
- 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 buildcompiles without errors -
cargo testpasses 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-Pattern | Problem | Solution |
|---|---|---|
| Blocking I/O in handlers | Blocks worker threads, poor concurrency | Use async/await with tokio-based clients |
| Shared mutable state without locks | Data races, undefined behavior | Use web::Data<Arc<Mutex<T>>> or RwLock |
| Middleware order confusion | Auth bypassed, CORS issues | Apply in order: Logger → Compress → CORS → Auth |
Raw String errors | No HTTP status mapping, unclear errors | Use custom AppError enum with ResponseError trait |
| Database connections per request | Connection exhaustion | Use connection pool in app state (PgPool) |
| Hardcoded configuration | Deployment issues, secrets exposed | Load config from environment variables |
| No request validation | SQL injection, XSS vulnerabilities | Use 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