RustのWebアプリケーションの開発に慣れるためRustのactix-webとdieselを使ってMedium.comクローンを作ってみました。作り方は、 https://github.com/gothinkster/realworld リポジトリのRustの実装を写経しました。この、Medium.comクローンではAPIは20APIほどあるので、モジュール化されたより実践的なRustのWebアプリケーションの作り方を学べます。 ぜひ、Rustの学習でより実践的なアプリケーションが作りたい方は参考にしてみてください。
Realworldリポジトリとは
RealWorld は、React、Anguler、Node、Djangoなどさまざまなプログラミング言語やフレームワークで実装されたMedium.comクローン(Conduitと呼ばれる)です。 リポジトリのREADMEをみると「ほとんどのTodoデモアプリは、フレームワークの機能をざっと紹介するが、実際にアプリケーションを構築するために必要な知識や視点は教えてくれない」という課題感からRealWorldをはじめたようです。
RealWorldでは、さまざまなフロントエンド実装(React、Anguler、Vue.jsなど)とさまざまなバックエンド実装(Node、Django、Ruby on Railsなど)があり、より実践的なディレクトリ構成、CRUD操作、認証、ページネーションなどのサンプルコードを利用できます。
具体的には CodebaseShow でMedium.comクローン(Conduit)の実装コードを検索できるようになっています。
RealWorldでできるようになること
RealWorldのRustのactix-webとdieselを利用した https://github.com/snamiki1212/realworld-v1-rust-actix-web-diesel を写経して、RustでWeb APIを開発しました。その中でどのようなことが学べたか説明します。
参考: 写経したコード https://github.com/nipe0324/learning-rust/tree/main/actix-web-diesel
20APIを実装するのでRust、actix-web、dieselの実装に慣れる
以下のようにAPIのルーティングをみると、20APIを実装します。APIによっては認証が必要なものがあったり、ユーザ、記事、コメント、タグなど複数のリソースに対するCRUD操作やページネーションなどの実装方法についても学べます。
あとは、APIが20本あり数が多いので何度もAPIを実装する中でRustでの実装に慣れることができます。
// src/routes.rs pub fn api(cfg: &mut ServiceConfig) { cfg.service( scope("/api") .route("/healthcheck", get().to(get_healthcheck)) .route("/users/login", post().to(signin)) .route("/users", post().to(signup)) .route("/user", get().to(get_user)) .route("/user", put().to(update_user)) .route("/profiles/{username}", get().to(get_profile)) .route("/profiles/{username}/follow", post().to(create_follow)) .route("/profiles/{username}/follow", delete().to(delete_follow)) .route("/articles", get().to(get_articles)) .route("/articles", post().to(create_article)) .route("/articles/feed", get().to(get_articles_feed)) .route("/articles/{slug}", get().to(get_article_by_slug)) .route("/articles/{slug}", put().to(update_article)) .route("/articles/{slug}", delete().to(delete_article)) .route("/articles/{slug}/comments", get().to(get_article_comments)) .route( "/articles/{slug}/comments", post().to(create_article_comment), ) .route( "/articles/{slug}/comments/{id}", delete().to(delete_article_comment), ) .route("/tags", get().to(get_tags)) .route("/articles/{slug}/favorite", post().to(create_favorite)) .route("/articles/{slug}/favorite", delete().to(delete_favorite)), ); }
周辺のクレートについても理解を深めることができる
Webアプリケーションを作った方はわかると思いますが、Webアプリケーションを作るにはWebアプリケーションフレームワークやDBライブラリ以外にも周辺のライブラリが必要になってきます。そういった周辺ライブラリについても必要に応じて調べながら学ぶことができます。
今回使ったクレートは以下の通りです。actix-web
とdiesel
以外にもよく使われるserde
やchrono
、エラーハンドリングを楽にするanyhow
やthiserror
などさまざまなクレートがあります
# Cargo.toml [dependencies] # web framework actix-web = "4.3.1" actix-cors = "0.6.4" # ORM and Query Builder diesel = { version = "2.0.4", features = ["r2d2", "postgres", "chrono", "uuid", "serde_json"] } # serialization / deserialization serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" # encode and decode JWTs jsonwebtoken = "8.3.0" # hash and verify password bcrypt = "0.14.0" # deta and time libray chrono = { version = "0.4.24", features = ["serde"] } # generate and parse UUIDs uuid = { version = "1.3.2", features = ["serde", "v4"] } # Flexible concrete Error type build on std::error::Error anyhow = "1.0.71" # derive(Error) thiserror = "1.0.40" # lightweight logging facade log = "0.4.17" env_logger = "0.10.0" # futures and streams featuring futures = "0.3.28" # dotenv dotenvy = "0.15.7" # Convert strings into any case convert_case = "0.6.0"
認証エラーやDBエラーをHTTPレスポンスに変換する方法がわかる
https://github.com/snamiki1212/realworld-v1-rust-actix-web-diesel の実装においては、認証エラーやDBエラーをAppError
に変換し、AppError
上にactix_web::error::ResponseError
トレイトを定義することで実現していました。
まず、アプリケーションエラーをAppError
として定義します。
// src/error.rs // アプリケーションエラー pub enum AppError { // 401 #[error("Unauthorized: {}", _0)] Unauthorized(JsonValue), // 403 #[error("Forbidden: {}", _0)] Forbidden(JsonValue), // 404 #[error("Not Found: {}", _0)] NotFound(JsonValue), // 422 #[error("Unprocessable Entity: {}", _0)] UnprocessableEntity(JsonValue), // 500 #[error("Internal Server Error")] InternalServerError, }
そして、次に、JWTやDBのエラーが発生した場合にAppError
に変換するためにFrom
トレイトを実装します。
// src/error.rs // JwtErrorをAppErrorに変換する impl From<JwtError> for AppError { fn from(err: JwtError) -> Self { match err.kind() { JwtErrorKind::InvalidToken => AppError::Unauthorized(json!({ "error": "Token is invalid" })), JwtErrorKind::InvalidIssuer => AppError::Unauthorized(json!({ "error": "Issuer is invalid", })), _ => AppError::Unauthorized(json!({ "error": "An issue was found with the token provided", })), } } } // DieselErrorをAppErrorに変換する impl From<DieselError> for AppError { fn from(err: DieselError) -> Self { match err { DieselError::DatabaseError(kind, info) => { if let DatabaseErrorKind::UniqueViolation = kind { let message = info.details().unwrap_or_else(|| info.message()).to_string(); AppError::UnprocessableEntity(json!({ "error": message })) } else { AppError::InternalServerError } } DieselError::NotFound => { AppError::NotFound(json!({ "error": "requested record was not found" })) } _ => AppError::InternalServerError, } } }
そして、最後にAppError
をactix-web
のエラーレスポンスに変換します。
// src/error.rs // AppErrorからactix-webのエラーレスポンスに変換する impl actix_web::error::ResponseError for AppError { fn error_response(&self) -> HttpResponse { match self { AppError::Unauthorized(ref msg) => HttpResponse::Unauthorized().json(msg), AppError::Forbidden(ref msg) => HttpResponse::Forbidden().json(msg), AppError::NotFound(ref msg) => HttpResponse::NotFound().json(msg), AppError::UnprocessableEntity(ref msg) => HttpResponse::UnprocessableEntity().json(msg), AppError::InternalServerError => { HttpResponse::InternalServerError().json("Internal Server Error") } } } fn status_code(&self) -> StatusCode { match *self { AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED, AppError::Forbidden(_) => StatusCode::FORBIDDEN, AppError::NotFound(_) => StatusCode::NOT_FOUND, AppError::UnprocessableEntity(_) => StatusCode::UNPROCESSABLE_ENTITY, AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, } } }
RealWorldで学ぶうえでの注意点
RealWorldのMedium.comクローンから学べることはたくさんあります。しかし、学ぶ上でも注意することはあります。Medium.comクローンはあくまでデモアプリ、かつ、各言語やフレームワークで実装をしていの人は別々の人なので、クラス設計やフレームワークの使い方や実装方法が必ずしも正しいとは限りません。そこは注意しつつ開発が必要です。困ったらクレートのREADMEやチュートリアルなどを確認しにいくのがよいと思います。
ぜひ、新しい言語や新しいフレームワークなどを学ぶ際には、チュートリアルのあとにRealWorldのクローンを作って手に馴染ませてみてください!!
参考サイト
- GitHub - gothinkster/realworld: "The mother of all demo apps" — Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more
- GitHub - actix/actix-web: Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
- GitHub - diesel-rs/diesel: A safe, extensible ORM and Query Builder for Rust
- Errors | Actix