Actix-web
最高性能を誇るRust Webフレームワーク。アクターモデルベースで並行処理に優れ、ベンチマークでトップクラスの性能を発揮。
GitHub概要
actix/actix-web
Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
ホームページ:https://actix.rs
スター23,255
ウォッチ234
フォーク1,756
作成日:2017年9月30日
言語:Rust
ライセンス:Apache License 2.0
トピックス
actixactix-webasyncrustwebweb-developmentwebsockets
スター履歴
データ取得日時: 2025/7/19 08:06
フレームワーク
Actix-web
概要
Actix-webは、Rust言語で開発された高性能なWebフレームワークです。アクターモデルをベースとし、非同期処理とメモリ安全性を両立しています。
詳細
Actix-web(アクティックス・ウェブ)は2017年にRust言語向けのWebフレームワークとして開発が開始されました。当初はアクターモデルを基盤とするactixフレームワーク上で構築されていましたが、現在はTokioとの互換性を高めて進化しています。TechEmpower Framework Benchmarkにおいて継続的に最高レベルのパフォーマンスを記録しており、RustのWebフレームワークの中でも特に高速です。HTTP/1.x、HTTP/2、WebSocketの完全サポート、強力なルーティングシステム、柔軟なミドルウェアアーキテクチャ、透過的なコンテンツ圧縮/解凍機能を標準搭載しています。型安全な抽出器(Extractor)システムによりリクエストデータを安全に処理でき、レスポンダー(Responder)トレイトにより柔軟なレスポンス生成が可能です。豊富な認証オプション、データベース統合、テンプレートエンジン対応など、エンタープライズ級の機能を提供しながら、ゼロコスト抽象化により高性能を維持しています。
メリット・デメリット
メリット
- 極めて高いパフォーマンス: TechEmpowerベンチマークで最高クラスの性能
- メモリ安全性: Rustの型システムによるコンパイル時エラー検出
- 非同期処理: Tokio基盤の効率的な非同期I/O処理
- 豊富な機能: HTTP/2、WebSocket、ミドルウェア、TLSサポート
- 型安全性: コンパイル時の厳格な型チェックによる実行時エラー削減
- モジュール設計: 必要な機能のみを選択できる柔軟なアーキテクチャ
- 活発な開発: 継続的な改善とコミュニティサポート
デメリット
- 学習コスト: Rust言語自体とライフタイム概念の理解が必要
- コンパイル時間: Rustプロジェクトの長いコンパイル時間
- エコシステム: 他言語比較でのライブラリエコシステムの相対的小ささ
- メモリ使用量: 高機能ゆえの基本的なメモリ消費量
- デバッグ複雑性: 非同期コードとライフタイムのデバッグ難易度
主要リンク
- Actix-web公式サイト
- Actix-web公式ドキュメント
- Actix-web GitHub リポジトリ
- Actix-web Examples
- Actix-web コミュニティ
- awc HTTPクライアント
書き方の例
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] // または #[tokio::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(greet)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
ルーティングとエクストラクタ
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,
}
// パスパラメータ
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_id),
email: format!("user{}@example.com", user_id),
};
Ok(HttpResponse::Ok().json(user))
}
// クエリパラメータ
async fn search_users(query: web::Query<Info>) -> Result<impl Responder> {
Ok(HttpResponse::Ok().json(format!(
"名前: {}, 年齢: {}で検索中...",
query.name, query.age
)))
}
// JSONボディ
async fn create_user(user: web::Json<Info>) -> Result<impl Responder> {
println!("新しいユーザーを作成: {:?}", user);
let new_user = User {
id: 1,
name: user.name.clone(),
email: format!("{}@example.com", user.name),
};
Ok(HttpResponse::Created().json(new_user))
}
// フォームデータ
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!(
"ユーザー{}を更新: 名前={}, 年齢={}",
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
}
ミドルウェアの使用
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;
// カスタムミドルウェア
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 {
// 認証ヘッダーをチェック
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" {
// 認証成功、リクエストを続行
let res = service.call(req).await?;
Ok(res)
} else {
// 無効なトークン
let (req, _) = req.into_parts();
let res = HttpResponse::Unauthorized()
.json("無効な認証トークンです")
.map_into_right_body();
Ok(ServiceResponse::new(req, res))
}
} else {
// 認証ヘッダーなし
let (req, _) = req.into_parts();
let res = HttpResponse::Unauthorized()
.json("認証が必要です")
.map_into_right_body();
Ok(ServiceResponse::new(req, res))
}
})
}
}
async fn protected_resource() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json("保護されたリソースにアクセス中"))
}
async fn public_resource() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json("パブリックリソース"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
HttpServer::new(|| {
App::new()
// グローバルミドルウェア
.wrap(Logger::default())
.wrap(NormalizePath::trim())
.wrap(DefaultHeaders::new().add(("X-Version", "1.0")))
.wrap(Compress::default())
// パブリックリソース
.route("/public", web::get().to(public_resource))
// 保護されたリソース(認証ミドルウェア付き)
.service(
web::scope("/api")
.wrap(Authentication)
.route("/protected", web::get().to(protected_resource))
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
データベース統合とテスト
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,
}
// データベースから全ユーザーを取得
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!("データベースエラー: {}", e);
Ok(HttpResponse::InternalServerError().json("データベースエラー"))
}
}
}
// 新しいユーザーを作成
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!("ユーザー作成エラー: {}", e);
Ok(HttpResponse::BadRequest().json("ユーザー作成に失敗しました"))
}
}
}
// 特定のユーザーを取得
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("ユーザーが見つかりません"))
}
Err(e) => {
eprintln!("データベースエラー: {}", e);
Ok(HttpResponse::InternalServerError().json("データベースエラー"))
}
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
// データベース接続プールを作成
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("データベースに接続できませんでした");
// テーブルを作成(実際のアプリケーションではマイグレーションを使用)
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("テーブル作成に失敗しました");
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() {
// テスト用のデータベースプールを作成
let pool = PgPool::connect("sqlite::memory:")
.await
.expect("テストデータベースの作成に失敗");
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とファイルアップロード
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アクター
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!("受信: {}", text);
ctx.text(format!("エコー: {}", text))
}
Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
Ok(ws::Message::Close(reason)) => {
ctx.close(reason);
ctx.stop();
}
_ => (),
}
}
}
// WebSocketハンドラー
async fn websocket(req: actix_web::HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
ws::start(MyWebSocket, &req, stream)
}
// ファイルアップロードハンドラー
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));
// ファイルを作成
let mut file = File::create(&filepath).await?;
// チャンクずつファイルに書き込み
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("ファイルがアップロードされませんでした"))
} else {
Ok(HttpResponse::Ok().json(serde_json::json!({
"message": "ファイルアップロード成功",
"files": uploaded_files
})))
}
}
// ファイルリストの取得
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))
}
// ルートハンドラー
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とファイルアップロードデモ</h1>
<h2>WebSocketテスト</h2>
<div>
<input type="text" id="messageInput" placeholder="メッセージを入力">
<button onclick="sendMessage()">送信</button>
</div>
<div id="messages"></div>
<h2>ファイルアップロード</h2>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" multiple>
<button type="submit">アップロード</button>
</form>
<h2>アップロード済みファイル</h2>
<div id="fileList"></div>
<script>
// WebSocket接続
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 = '';
}
// ファイルリストを取得
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();
// アップロードディレクトリを作成
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
}
エラーハンドリングとレスポンス管理
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,
}
// カスタムエラー型
#[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, "バリデーションエラー: {}", msg),
AppError::DatabaseError(msg) => write!(f, "データベースエラー: {}", msg),
AppError::NotFoundError(msg) => write!(f, "見つかりません: {}", msg),
AppError::AuthenticationError => write!(f, "認証エラー"),
}
}
}
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(_) => "内部サーバーエラーが発生しました".to_string(),
AppError::NotFoundError(msg) => msg.clone(),
AppError::AuthenticationError => "認証が必要です".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,
}
// バリデーション機能付きハンドラー
async fn create_user_with_validation(user: web::Json<UserInput>) -> Result<HttpResponse, AppError> {
// バリデーション
if user.name.is_empty() {
return Err(AppError::ValidationError("名前は必須です".to_string()));
}
if user.age < 18 {
return Err(AppError::ValidationError("18歳以上である必要があります".to_string()));
}
if !user.email.contains('@') {
return Err(AppError::ValidationError("有効なメールアドレスを入力してください".to_string()));
}
// データベース操作をシミュレート
if user.email == "[email protected]" {
return Err(AppError::DatabaseError("データベース接続エラー".to_string()));
}
// 成功レスポンス
Ok(HttpResponse::Created().json(serde_json::json!({
"message": "ユーザーが正常に作成されました",
"user": {
"name": user.name,
"email": user.email,
"age": user.age
}
})))
}
// 認証が必要なハンドラー
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": "保護されたエンドポイントへのアクセス成功",
"data": "機密データ"
})))
} else {
Err(AppError::AuthenticationError)
}
}
_ => Err(AppError::AuthenticationError),
}
}
// 404エラーを見つけるハンドラー
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("ユーザーが見つかりません".to_string()));
}
Ok(HttpResponse::Ok().json(serde_json::json!({
"id": user_id,
"name": format!("ユーザー{}", user_id),
"email": format!("user{}@example.com", user_id)
})))
}
// グローバルエラーハンドラー
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: "要求されたリソースが見つかりません".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: "内部サーバーエラーが発生しました".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 エラーハンドリングデモ",
"endpoints": [
"POST /users - ユーザー作成(バリデーション付き)",
"GET /users/{id} - ユーザー取得(999で404エラー)",
"GET /protected - 保護されたエンドポイント(Bearer tokenが必要)"
]
}))
}))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}