Locoとは
RailsにインスパイアされたRust製のウェブアプリケーションフレームワークです。
主な機能はこんな感じです。Railsに慣れていればとても親しみやすい構成です。
※MVCではありますが、ViewはJSONがデフォルトなので、RailsのAPIモードのような感じです。
Shuttleとは
Rust用のHerokuのような感じです。Rustアプリケーションを簡単にデプロイできます。
Postgresqlなどのサービスもある程度無料で使えます。
Heroku同様、コマンド1つでデプロイできてSSLのURLが付与されて即アクセスできて便利です。
作ったもの
haikunator でランダムな命名を生成するサービスです。
調べれば他にいくらでもありそうではありますが、今回はRustの練習がてら作ってみました。
作っていく
環境構築
今回はGitHub Codespeces上にDevContainerで構築していきます。
Ctrl+P → Add Dev Container Configuration Files と選択していき、Rustのコンテナを構成します。
Dev Containerの構成ファイルができたので、Locoを使えるように少し修正していきます。
{
"name": "Loco",
"dockerComposeFile": "compose.yaml",
"postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"forwardPorts": [
5150, 5153
],
"features": {
"ghcr.io/devcontainers/features/node:1": {}
}
}
-
image
を削除し、Dockerfileとcompose.ymlを使うように変更する - フロントエンドはJSにしたいのでfeaturesでnodeを入れる
- フロントエンドのホットリロード用に5153ポートを空ける(5150はLoco用)
FROM mcr.microsoft.com/devcontainers/rust:1
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& cargo install loco-cli cargo-insta cargo-cache cargo-shuttle \
&& chown -R vscode /usr/local/cargo
COPY .env /.env
- LocoのCLIやShuttleのCLIなどを入れる
ここまで変更したら、Dev Containerをビルドします。
※すごく時間がかかるので、気長に待ちます。(15分くらい)
環境確認
ビルドが終わったら各種コマンドを確認してみます。
$ rustc --version
rustc 1.80.1 (3f5fd8dd4 2024-08-06)
$ cargo --version
cargo 1.80.1 (376290515 2024-07-16)
$ loco --version
loco-cli 0.2.8
$ cargo shuttle --version
cargo-shuttle 0.47.0
$ npm --version
10.8.2
$ pnpm --version
9.10.0
Loco new
loco new
コマンドで雛形を作ります。
lightweight-serviceはあまりにもminimalするぎるので、一旦SaaS Appを作成します。不要なものはあとで全部消します。
$ loco new
ディレクトリ構成
このような構成で生成されます。
フロントエンド
frontend/
ディレクトリにReactアプリが生成されているので、一旦そのままビルドします。
$ cd frontend
$ pnpm install
$ pnpm build
フロントエンドファイル配信設定
Rust側にあるstatic
フォルダを配信するかフロント側のdist/
フォルダを配信するか選べるので、今回はフロント側を有効にします。
#
# (1) Server-side static assets config
# ====================================
#
# for use with the view_engine in initializers/view_engine.rs
# static:
# enable: true
# must_exist: true
# precompressed: false
# folder:
# uri: "/static"
# path: "assets/static"
# fallback: "assets/static/404.html"
#
# (2) Client side app static config
# =================================
#
# Note that you need to go in `frontend` and run your frontend build first,
# e.g.: $ npm install & npm build
#
# (client-block-start)
static:
enable: true
must_exist: true
precompressed: false
folder:
uri: "/"
path: "frontend/dist"
fallback: "frontend/dist/index.html"
# (client-block-end)
#
Loco start
この状態で試しに起動してみます。
環境に合わせて以下どちらかで起動すると思います。
$ cargo loco start
$ cargo loco start --binding 0.0.0.0
無事表示できました。
GitHub CodeSpacesの場合は、アクセスしても表示されない場合があるので、ポート設定で「公開設定」を一瞬publicにしてまたすぐprivateにしたりしてみると何故かアクセスできるようになったりします。
バックエンドもちゃんと動いていそうです。
$ curl localhost:5150/_health
{"ok":true}
不要な機能の削除
現状、雛形で生成されたフロントエンドは何故かバックエンドとはなんの関係もないただのペライチでした。バックエンドにはauthやuserのAPIが実装されていますが、そこと連動した動作までは試せないようでした。
今回はその辺の機能は必要ないため、削除していきます。
また、database周りやworker、mailerなども不要なので削除していきます。
量が多いので省略しますが、差分はこんな感じです。
Haikunator Generator
環境整備が済んだのでアプリケーションを実装していきます。
ざっくり以下のような感じです。
API
use axum::debug_handler;
use loco_rs::prelude::*;
use crate::views::haikunator::GeneratorResponse;
#[debug_handler]
async fn generate() -> Result<Response> {
let generated = haikunator::Haikunator::default().haikunate();
format::json(GeneratorResponse::generate(&generated))
}
async fn generate_txt() -> Result<Response> {
let generated = haikunator::Haikunator::default().haikunate();
format::text(&generated)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("api")
.add("/gen", get(generate))
.add("/gen.txt", get(generate_txt))
}
フロントエンド
'use client'
import { useState } from 'react'
export const HaikunatorGenerator = () => {
const [generatedString, setGeneratedString] = useState<string>('')
const generateString = async () => {
try {
const response = await fetch('/api/gen')
if (response.ok) {
const data: { name: string } = await response.json()
setGeneratedString(data.name)
} else {
throw new Error('Failed to generate string')
}
} catch (error) {
console.error(error)
}
}
return (...)
}
404ページ
デフォルトのfallbackのままだと、存在しないパスにアクセスした場合にも全てindexページがレスポンスされてしまうので、404ページを返すようにします。
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
const root = document.getElementById("root");
if (!root) {
throw new Error("No root element found");
}
ReactDOM.createRoot(root).render(
<React.StrictMode>
<div className="flex items-center justify-center h-screen">
<h1 className="text-4xl font-bold text-gray-500">404 Not Found</h1>
</div>
</React.StrictMode>
);
# (client-block-start)
static:
...
fallback: "frontend/dist/404.html"
# (client-block-end)
他にもいくつか設定していくと、こんな感じに表示できました。
レスポンスステータスもきちんと404でした。
Shuttleにデプロイ
Shuttleへはとても簡単にデプロイできるので詳細は省略します。
注意点としては、ShuttleにはGit管理下のファイルしか転送できないため、フロントエンドのビルド成果物は一時的にでもGit管理下に追加する必要があります。
(あまりにも不便なので今後改善されると期待してます)
これを踏まえて、デプロイの流れは以下のようになります。
$ pushd frontend
$ echo !dist/ >> .gitignore
$ pnpm build
$ popd
$ cargo shuttle login
$ cargo shuttle deploy --allow-dirty
$ git checkout frontend/.gitignore
-
frontend/dist
を一時的にGit管理に追加(無視を無視) - フロントエンドをビルド
- Shuttleにログイン
- Shuttleにデプロイ
- 一時的な変更を元に戻す
ちょっとめんどいですが、デプロイも自動化する前提であれば大したことではない気もします。
おわり
以上です。
とても簡単にサービスをひとつリリースできてしまいました。
Locoの所感としては、Railsのようなフルスタックフレームワークというよりも、Rustでバックエンドを構築する際の雛形として優秀という印象でした。
今回のように、使わない機能は全て削除してしまえるし、雛形で用意されているモジュールが自分好みでなければ差し替えてしまうこともできるしといった感じで、柔軟に使えるような気がしました。