Actix-web
Highest-performing Rust web framework. Actor model-based with excellent concurrency, achieves top-class performance in benchmarks.
GitHub Overview
actix/actix-web
Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
Topics
Star History
Framework
Actix-web
Overview
Actix-web is a high-performance web framework developed in Rust. Based on the actor model, it combines asynchronous processing with memory safety.
Details
Actix-web was started in 2017 as a web framework for the Rust language. Initially built on the actix framework based on the actor model, it has now evolved to improve compatibility with Tokio. It consistently records top-level performance in the TechEmpower Framework Benchmark and is particularly fast among Rust web frameworks. It comes standard with full support for HTTP/1.x, HTTP/2, and WebSocket, a powerful routing system, flexible middleware architecture, and transparent content compression/decompression capabilities. It can safely handle request data through a type-safe extractor system and enables flexible response generation through the Responder trait. While providing enterprise-grade features such as rich authentication options, database integration, and template engine support, it maintains high performance through zero-cost abstractions.
Pros and Cons
Pros
- Extremely High Performance: Top-class performance in TechEmpower benchmarks
- Memory Safety: Compile-time error detection through Rust's type system
- Asynchronous Processing: Efficient async I/O processing based on Tokio
- Rich Features: HTTP/2, WebSocket, middleware, TLS support
- Type Safety: Reduced runtime errors through strict compile-time type checking
- Modular Design: Flexible architecture allowing selection of only needed features
- Active Development: Continuous improvement and community support
Cons
- Learning Cost: Requires understanding of Rust language and lifetime concepts
- Compile Time: Long compilation times for Rust projects
- Ecosystem: Relatively smaller library ecosystem compared to other languages
- Memory Usage: Basic memory consumption due to rich features
- Debug Complexity: Debugging difficulty of async code and lifetimes
Key Links
- Actix-web Official Site
- Actix-web Official Documentation
- Actix-web GitHub Repository
- Actix-web Examples
- Actix-web Community
- awc HTTP Client
Code Examples
Hello World
use actix_web::{get, web, App, HttpServer, Responder};
#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
format!("Hello {name}!")
}
#[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(greet)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Routing and Extractors
use actix_web::{web, App, HttpServer, Responder, Result, HttpResponse};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Info {
name: String,
age: u32,
}
#[derive(Serialize)]
struct User {
id: u32,
name: String,
email: String,
}
// Path parameters
async fn get_user(path: web::Path<u32>) -> Result<impl Responder> {
let user_id = path.into_inner();
let user = User {
id: user_id,
name: format!("User{}", user_id),
email: format!("user{}@example.com", user_id),
};
Ok(HttpResponse::Ok().json(user))
}
// Query parameters
async fn search_users(query: web::Query<Info>) -> Result<impl Responder> {
Ok(HttpResponse::Ok().json(format!(
"Searching for name: {}, age: {}",
query.name, query.age
)))
}
// JSON body
async fn create_user(user: web::Json<Info>) -> Result<impl Responder> {
println!("Creating new user: {:?}", user);
let new_user = User {
id: 1,
name: user.name.clone(),
email: format!("{}@example.com", user.name),
};
Ok(HttpResponse::Created().json(new_user))
}
// Form data
async fn update_user(
path: web::Path<u32>,
form: web::Form<Info>
) -> Result<impl Responder> {
let user_id = path.into_inner();
Ok(HttpResponse::Ok().json(format!(
"Updating user {}: name={}, age={}",
user_id, form.name, form.age
)))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/users/{id}", web::get().to(get_user))
.route("/users/search", web::get().to(search_users))
.route("/users", web::post().to(create_user))
.route("/users/{id}", web::put().to(update_user))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Using Middleware
use actix_web::{
dev::{ServiceRequest, ServiceResponse, Service, Transform},
web, App, HttpServer, HttpResponse, Result, Error,
middleware::{Logger, NormalizePath, DefaultHeaders, Compress},
http::header,
};
use futures_util::future::LocalBoxFuture;
use std::future::{ready, Ready};
use std::rc::Rc;
// Custom middleware
pub struct Authentication;
impl<S, B> Transform<S, ServiceRequest> for Authentication
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = AuthenticationMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthenticationMiddleware {
service: Rc::new(service),
}))
}
}
pub struct AuthenticationMiddleware<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for AuthenticationMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_web::dev::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let service = self.service.clone();
Box::pin(async move {
// Check authentication header
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok());
if let Some(token) = auth_header {
if token == "Bearer valid_token" {
// Authentication successful, continue request
let res = service.call(req).await?;
Ok(res)
} else {
// Invalid token
let (req, _) = req.into_parts();
let res = HttpResponse::Unauthorized()
.json("Invalid authentication token")
.map_into_right_body();
Ok(ServiceResponse::new(req, res))
}
} else {
// No authentication header
let (req, _) = req.into_parts();
let res = HttpResponse::Unauthorized()
.json("Authentication required")
.map_into_right_body();
Ok(ServiceResponse::new(req, res))
}
})
}
}
async fn protected_resource() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json("Accessing protected resource"))
}
async fn public_resource() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json("Public resource"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
HttpServer::new(|| {
App::new()
// Global middleware
.wrap(Logger::default())
.wrap(NormalizePath::trim())
.wrap(DefaultHeaders::new().add(("X-Version", "1.0")))
.wrap(Compress::default())
// Public resources
.route("/public", web::get().to(public_resource))
// Protected resources (with authentication middleware)
.service(
web::scope("/api")
.wrap(Authentication)
.route("/protected", web::get().to(protected_resource))
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Database Integration and Testing
use actix_web::{web, App, HttpServer, HttpResponse, Result, middleware::Logger};
use sqlx::{PgPool, Pool, Postgres, Row};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct User {
id: i32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
// Get all users from database
async fn get_users(pool: web::Data<PgPool>) -> Result<HttpResponse> {
let result = sqlx::query("SELECT id, name, email FROM users")
.fetch_all(pool.get_ref())
.await;
match result {
Ok(rows) => {
let users: Vec<User> = rows
.iter()
.map(|row| User {
id: row.get("id"),
name: row.get("name"),
email: row.get("email"),
})
.collect();
Ok(HttpResponse::Ok().json(users))
}
Err(e) => {
eprintln!("Database error: {}", e);
Ok(HttpResponse::InternalServerError().json("Database error"))
}
}
}
// Create new user
async fn create_user(
pool: web::Data<PgPool>,
user: web::Json<CreateUser>,
) -> Result<HttpResponse> {
let result = sqlx::query(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email"
)
.bind(&user.name)
.bind(&user.email)
.fetch_one(pool.get_ref())
.await;
match result {
Ok(row) => {
let new_user = User {
id: row.get("id"),
name: row.get("name"),
email: row.get("email"),
};
Ok(HttpResponse::Created().json(new_user))
}
Err(e) => {
eprintln!("User creation error: {}", e);
Ok(HttpResponse::BadRequest().json("Failed to create user"))
}
}
}
// Get specific user
async fn get_user(
pool: web::Data<PgPool>,
path: web::Path<i32>
) -> Result<HttpResponse> {
let user_id = path.into_inner();
let result = sqlx::query("SELECT id, name, email FROM users WHERE id = $1")
.bind(user_id)
.fetch_optional(pool.get_ref())
.await;
match result {
Ok(Some(row)) => {
let user = User {
id: row.get("id"),
name: row.get("name"),
email: row.get("email"),
};
Ok(HttpResponse::Ok().json(user))
}
Ok(None) => {
Ok(HttpResponse::NotFound().json("User not found"))
}
Err(e) => {
eprintln!("Database error: {}", e);
Ok(HttpResponse::InternalServerError().json("Database error"))
}
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
// Create database connection pool
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://user:password@localhost/mydb".to_string());
let pool = PgPool::connect(&database_url)
.await
.expect("Failed to connect to database");
// Create table (use migrations in real applications)
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE NOT NULL
)
"#
)
.execute(&pool)
.await
.expect("Failed to create table");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(pool.clone()))
.wrap(Logger::default())
.route("/users", web::get().to(get_users))
.route("/users", web::post().to(create_user))
.route("/users/{id}", web::get().to(get_user))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{test, App};
#[actix_web::test]
async fn test_get_users() {
// Create test database pool
let pool = PgPool::connect("sqlite::memory:")
.await
.expect("Failed to create test database");
let app = test::init_service(
App::new()
.app_data(web::Data::new(pool))
.route("/users", web::get().to(get_users))
).await;
let req = test::TestRequest::get()
.uri("/users")
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
}
WebSocket and File Upload
use actix_web::{
web, App, HttpServer, HttpResponse, Result, Error,
middleware::Logger,
};
use actix_files as fs;
use actix_multipart::Multipart;
use actix_web_actors::ws;
use futures_util::StreamExt as _;
use std::io::Write;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
// WebSocket actor
struct MyWebSocket;
impl actix::Actor for MyWebSocket {
type Context = ws::WebsocketContext<Self>;
}
impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Text(text)) => {
println!("Received: {}", text);
ctx.text(format!("Echo: {}", text))
}
Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
Ok(ws::Message::Close(reason)) => {
ctx.close(reason);
ctx.stop();
}
_ => (),
}
}
}
// WebSocket handler
async fn websocket(req: actix_web::HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
ws::start(MyWebSocket, &req, stream)
}
// File upload handler
async fn upload_file(mut payload: Multipart) -> Result<HttpResponse> {
let mut uploaded_files = Vec::new();
while let Some(item) = payload.next().await {
let mut field = item?;
let content_disposition = field.content_disposition();
if let Some(filename) = content_disposition.get_filename() {
let filepath = format!("./uploads/{}", sanitize_filename::sanitize(&filename));
// Create file
let mut file = File::create(&filepath).await?;
// Write file chunk by chunk
while let Some(chunk) = field.next().await {
let data = chunk?;
file.write_all(&data).await?;
}
uploaded_files.push(filename.to_string());
}
}
if uploaded_files.is_empty() {
Ok(HttpResponse::BadRequest().json("No files were uploaded"))
} else {
Ok(HttpResponse::Ok().json(serde_json::json!({
"message": "File upload successful",
"files": uploaded_files
})))
}
}
// Get file list
async fn list_files() -> Result<HttpResponse> {
let mut files = Vec::new();
if let Ok(mut entries) = tokio::fs::read_dir("./uploads").await {
while let Some(entry) = entries.next_entry().await? {
if let Some(filename) = entry.file_name().to_str() {
files.push(filename.to_string());
}
}
}
Ok(HttpResponse::Ok().json(files))
}
// Root handler
async fn index() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().content_type("text/html").body(r#"
<!DOCTYPE html>
<html>
<head>
<title>Actix-web Demo</title>
<meta charset="utf-8">
</head>
<body>
<h1>Actix-web WebSocket and File Upload Demo</h1>
<h2>WebSocket Test</h2>
<div>
<input type="text" id="messageInput" placeholder="Enter message">
<button onclick="sendMessage()">Send</button>
</div>
<div id="messages"></div>
<h2>File Upload</h2>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" multiple>
<button type="submit">Upload</button>
</form>
<h2>Uploaded Files</h2>
<div id="fileList"></div>
<script>
// WebSocket connection
const ws = new WebSocket('ws://127.0.0.1:8080/ws');
const messages = document.getElementById('messages');
ws.onmessage = function(event) {
const div = document.createElement('div');
div.textContent = event.data;
messages.appendChild(div);
};
function sendMessage() {
const input = document.getElementById('messageInput');
ws.send(input.value);
input.value = '';
}
// Get file list
fetch('/files')
.then(response => response.json())
.then(files => {
const fileList = document.getElementById('fileList');
files.forEach(file => {
const div = document.createElement('div');
div.innerHTML = `<a href="/static/${file}" target="_blank">${file}</a>`;
fileList.appendChild(div);
});
});
</script>
</body>
</html>
"#))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
// Create uploads directory
tokio::fs::create_dir_all("./uploads").await?;
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.route("/", web::get().to(index))
.route("/ws", web::get().to(websocket))
.route("/upload", web::post().to(upload_file))
.route("/files", web::get().to(list_files))
.service(fs::Files::new("/static", "./uploads").show_files_listing())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Error Handling and Response Management
use actix_web::{
web, App, HttpServer, HttpResponse, Result, ResponseError,
middleware::{ErrorHandlerResponse, ErrorHandlers},
http::{StatusCode, header::ContentType},
dev::ServiceResponse,
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
#[derive(Debug, Serialize)]
struct ErrorResponse {
error: String,
message: String,
code: u16,
}
// Custom error type
#[derive(Debug)]
enum AppError {
ValidationError(String),
DatabaseError(String),
NotFoundError(String),
AuthenticationError,
}
impl Display for AppError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AppError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
AppError::NotFoundError(msg) => write!(f, "Not found: {}", msg),
AppError::AuthenticationError => write!(f, "Authentication error"),
}
}
}
impl ResponseError for AppError {
fn status_code(&self) -> StatusCode {
match self {
AppError::ValidationError(_) => StatusCode::BAD_REQUEST,
AppError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::NotFoundError(_) => StatusCode::NOT_FOUND,
AppError::AuthenticationError => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse {
let error_response = ErrorResponse {
error: self.to_string(),
message: match self {
AppError::ValidationError(msg) => msg.clone(),
AppError::DatabaseError(_) => "Internal server error occurred".to_string(),
AppError::NotFoundError(msg) => msg.clone(),
AppError::AuthenticationError => "Authentication required".to_string(),
},
code: self.status_code().as_u16(),
};
HttpResponse::build(self.status_code())
.insert_header(ContentType::json())
.json(error_response)
}
}
#[derive(Deserialize)]
struct UserInput {
name: String,
email: String,
age: u32,
}
// Handler with validation
async fn create_user_with_validation(user: web::Json<UserInput>) -> Result<HttpResponse, AppError> {
// Validation
if user.name.is_empty() {
return Err(AppError::ValidationError("Name is required".to_string()));
}
if user.age < 18 {
return Err(AppError::ValidationError("Must be 18 or older".to_string()));
}
if !user.email.contains('@') {
return Err(AppError::ValidationError("Please enter a valid email address".to_string()));
}
// Simulate database operation
if user.email == "[email protected]" {
return Err(AppError::DatabaseError("Database connection error".to_string()));
}
// Success response
Ok(HttpResponse::Created().json(serde_json::json!({
"message": "User created successfully",
"user": {
"name": user.name,
"email": user.email,
"age": user.age
}
})))
}
// Handler requiring authentication
async fn protected_endpoint(req: actix_web::HttpRequest) -> Result<HttpResponse, AppError> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
if token == "Bearer valid_token" {
Ok(HttpResponse::Ok().json(serde_json::json!({
"message": "Successfully accessed protected endpoint",
"data": "Confidential data"
})))
} else {
Err(AppError::AuthenticationError)
}
}
_ => Err(AppError::AuthenticationError),
}
}
// Handler to find user
async fn find_user(path: web::Path<u32>) -> Result<HttpResponse, AppError> {
let user_id = path.into_inner();
if user_id == 999 {
return Err(AppError::NotFoundError("User not found".to_string()));
}
Ok(HttpResponse::Ok().json(serde_json::json!({
"id": user_id,
"name": format!("User{}", user_id),
"email": format!("user{}@example.com", user_id)
})))
}
// Global error handlers
fn json_error_handler() -> ErrorHandlers<actix_web::body::BoxBody> {
ErrorHandlers::new()
.handler(StatusCode::NOT_FOUND, not_found_handler)
.handler(StatusCode::INTERNAL_SERVER_ERROR, internal_error_handler)
}
fn not_found_handler<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>, Error> {
let (req, _) = res.into_parts();
let error_response = ErrorResponse {
error: "Not Found".to_string(),
message: "The requested resource was not found".to_string(),
code: 404,
};
let res = HttpResponse::NotFound()
.json(error_response)
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(ServiceResponse::new(req, res)))
}
fn internal_error_handler<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>, Error> {
let (req, _) = res.into_parts();
let error_response = ErrorResponse {
error: "Internal Server Error".to_string(),
message: "An internal server error occurred".to_string(),
code: 500,
};
let res = HttpResponse::InternalServerError()
.json(error_response)
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(ServiceResponse::new(req, res)))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(actix_web::middleware::Logger::default())
.wrap(json_error_handler())
.route("/users", web::post().to(create_user_with_validation))
.route("/users/{id}", web::get().to(find_user))
.route("/protected", web::get().to(protected_endpoint))
.route("/", web::get().to(|| async {
HttpResponse::Ok().json(serde_json::json!({
"message": "Actix-web Error Handling Demo",
"endpoints": [
"POST /users - Create user (with validation)",
"GET /users/{id} - Get user (404 error with 999)",
"GET /protected - Protected endpoint (requires Bearer token)"
]
}))
}))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}