RustのWebアプリ開発に慣れるためRustのactix-webとdieselを使ってMedium.comクローンを作ってみた
RustのWebアプリケーションの開発に慣れるためRustのactix-webとdieselを使ってMedium.comクローンを作ってみました。 Medium.comクローンではAPIは20APIほどあるので、モジュール化されたより実践的なRustのWebアプリケーションの作り方を学べます。 ぜひ、Rustの学習でより実践的なアプリケーションが作りたい方は参考にしてみてください。
Realworldリポジトリとは
RealWorld は、React、Anguler、Node、Django などさまざまなプログラミング言語やフレームワークで実装されたMedium.comクローンのプロジェクトのことです。
リポジトリのREADMEをみると「ほとんどのTodoデモアプリは、フレームワークの機能をざっと紹介するが、実際にアプリケーションを構築するために必要な知識や視点は教えてくれない」という課題感からRealWorldをはじめたようです。
RealWorldでは、さまざまなフロントエンド実装(React、Anguler、Vue.jsなど)とさまざまなバックエンド実装(Node、Django、Ruby on Railsなど)があり、より実践的なディレクトリ構成、CRUD操作、認証、ページネーションなどのサンプルコードを利用できます。
CodebaseShow のページから色々な実装のコードを検索できます。
RealWorldでできるようになること
RealWorldのRustのactix-webとdieselを利用した snamiki1212/realworld-v1-rust-actix-web-diesel を写経して、RustでWeb APIを開発しました。その中で次のようなことを学べました
- 約20のAPI実装をすることで、Rust, actix-web, dieselに慣れる
- よく使うクレートについて理解を深めることができる
- 認証エラーやDBエラーをHTTPレスポンスに変換する方法がわかる
約20のAPI実装をすることで、Rust, actix-web, dieselに慣れる
約20のAPIを実装します。
APIによっては認証が必要なものがあったり、ユーザ、記事、コメント、タグなど複数のリソースに対するCRUD操作やページネーションなどの実装方法についても学べます。
何度もAPIを実装する中でRustでの実装に慣れることができます。
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アプリケーションフレームワークやDBライブラリ以外にも周辺のライブラリが必要になってきます。
そういった周辺ライブラリ(Rustの場合「クレート」と呼ぶ)についても学ぶことができます。
今回使ったクレートは以下の通りで、よく使われる serde や chrono、エラーハンドリングを楽にする anyhow や thiserror などについて学べます。
[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レスポンスに変換する方法がわかる
認証エラーやDBエラーを AppError
に変換し、AppError
上に actix_web::error::ResponseError
トレイトを定義することで実現していました。
// アプリケーションエラーの定義
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
トレイトを実装します。
// Fromトレイトの実装
// 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
のエラーレスポンスに変換します。
// 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のクローンを作って手に馴染ませてみてください!!