この設計図を分析して、問題点と改善案を解説します。
全体構造の理解
この図は、Webアプリケーションのアーキテクチャを示しており、大きく以下の層で構成されています:
- Configuration層
- WWW/UI層
- Application層
- Abstraction層
- Custom層
主な問題点
1. レイヤー間の依存関係が不明瞭
- 矢印の方向性や意味が統一されていない
- どのコンポーネントがどこに依存しているか追跡しづらい
- 特にApplication層からAbstraction層への接続が複雑
2. 責務の分離が曖昧
- CI ToolsがApplication層に配置されているのは不自然
- WebpackがAbstraction層とCustom層の両方に存在し、役割が重複している可能性
- Static Assets & JSSの位置づけが不明確
3. セキュリティ上の懸念
- HTTP ServerとRouterの後にミドルウェア層がある構成は、認証・認可のタイミングが遅い可能性
- 直接DBへの接続経路が多すぎる(複数のコンポーネントから直接アクセス)
4. スケーラビリティの問題
- Migrationがデータベースと直接接続されているが、環境管理が不明
- キャッシュ層が見当たらない
- 負荷分散の仕組みが示されていない
5. テストの位置づけ
- CI ToolsとTest Harnessの関係性が不明確
- どの層をどうテストするのか戦略が見えない
改善提案
アーキテクチャの再構成
┌─────────────────────────────────────┐
│ Infrastructure Layer │
│ - Config Loader (環境変数管理) │
│ - CI/CD Pipeline │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Presentation Layer │
│ - Express + Router │
│ - Middleware (認証/認可/ログ) │
│ - Static Assets Serving │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Application Layer │
│ - Business Logic │
│ - Migration Scripts │
│ - JWT処理 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Domain Layer │
│ - Models & DTOs │
│ - Domain Services │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Data Access Layer │
│ - Repository Pattern │
│ - Database/SQLite Abstraction │
│ - Cache Layer (Redis等) │
└─────────────────────────────────────┘
具体的な改善案
- レイヤードアーキテクチャの徹底
- 各層の責務を明確化
- 上位層は下位層にのみ依存
- インターフェースを通じた疎結合化
- セキュリティ強化
- ミドルウェアをルーティング直後に配置
- JWT検証を早期に実施
- DBアクセスはRepository層に集約
- テスト戦略の明確化
- 単体テスト:各層で独立実施
- 統合テスト:Test Harnessで実施
- E2Eテスト:CI/CDパイプラインに統合
- ビルドプロセスの整理
- Webpackは開発環境でのみ使用
- 本番環境は事前ビルド済みアセットを使用
- バンドルサイズの最適化
- 可観測性の追加
- ログ集約システム
- メトリクス収集(APM)
- ヘルスチェックエンドポイント
優先順位
- 高優先度:セキュリティとレイヤー分離
- 中優先度:テスト環境の整備
- 低優先度:パフォーマンス最適化
この設計図は基本的な構造は押さえていますが、実装詳細と運用面での考慮が不足しています。特にエンタープライズ環境で使用する場合は、上記の改善を検討することをお勧めします。
このRust、Locoの設計図や改善提案から、ファイルの修正や追加、削除などの修正方法を解説してください。
Rust + Locoフレームワークの設計図に基づいた、具体的なファイル構成と修正方法を解説します。
📂 推奨ディレクトリ構成(Rust + Loco)
Locoプロジェクトの推奨ファイル構成
project_root/ ├── config/ │ ├── development.yaml │ ├── production.yaml │ └── test.yaml ├── src/ │ ├── app.rs │ ├── main.rs │ ├── controllers/ │ │ ├── mod.rs │ │ ├── auth.rs │ │ └── api.rs │ ├── models/ │ │ ├── mod.rs │ │ ├── users.rs │ │ └── _entities/ │ ├── views/ │ │ ├── mod.rs │ │ └── auth.rs │ ├── workers/ │ │ ├── mod.rs │ │ └── background_job.rs │ ├── middleware/ │ │ ├── mod.rs │ │ ├── auth.rs │ │ └── logging.rs │ ├── services/ │ │ ├── mod.rs │ │ └── business_logic.rs │ └── tests/ │ ├── mod.rs │ └── integration/ ├── migration/ │ └── src/ │ ├── lib.rs │ ├── m20240101_000001_create_users.rs │ └── mod.rs ├── frontend/ │ ├── package.json │ ├── webpack.config.js │ └── src/ ├── assets/ │ └── static/ ├── tests/ │ └── requests/ └── Cargo.toml
1. 設定ファイルの整理
削除すべきファイル
- 環境別の設定が混在している場合は統合
config/development.yaml (修正)
yaml
server:
port: 5150
host: 0.0.0.0
database:
uri: sqlite://loco_app.sqlite?mode=rwc
enable_logging: true
auto_migrate: true
auth:
jwt:
secret: "development-secret-change-in-production"
expiration: 3600
middleware:
cors:
enabled: true
allow_origins:
- "http://localhost:3000"
rate_limit:
enabled: false
logging:
level: debug
config/production.yaml (追加)
yaml
server:
port: ${PORT:5150}
host: 0.0.0.0
database:
uri: ${DATABASE_URL}
enable_logging: false
connection_pool:
max_connections: 20
min_connections: 5
auth:
jwt:
secret: ${JWT_SECRET}
expiration: 3600
middleware:
cors:
enabled: true
allow_origins:
- ${FRONTEND_URL}
rate_limit:
enabled: true
requests_per_minute: 60
logging:
level: info
2. ミドルウェア層の実装
src/middleware/mod.rs (新規作成)
rust
pub mod auth; pub mod logging; pub mod rate_limit; pub use auth::AuthMiddleware; pub use logging::RequestLogger; pub use rate_limit::RateLimiter;
src/middleware/auth.rs (新規作成)
rust
use axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::Response,
};
use loco_rs::prelude::*;
pub struct AuthMiddleware;
impl AuthMiddleware {
pub async fn verify_jwt(
req: Request,
next: Next,
) -> Result {
// Authorizationヘッダーの取得
let auth_header = req.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| Error::Unauthorized)?;
// "Bearer "プレフィックスの確認
if !auth_header.starts_with("Bearer ") {
return Err(Error::Unauthorized);
}
let token = &auth_header[7..];
// JWT検証ロジック
// TODO: 実際のJWT検証を実装
Ok(next.run(req).await)
}
}
src/middleware/logging.rs (新規作成)
rust
use axum::{
extract::Request,
middleware::Next,
response::Response,
};
use tracing::info;
pub struct RequestLogger;
impl RequestLogger {
pub async fn log(req: Request, next: Next) -> Response {
let method = req.method().clone();
let uri = req.uri().clone();
let start = std::time::Instant::now();
let response = next.run(req).await;
let duration = start.elapsed();
let status = response.status();
info!(
method = %method,
uri = %uri,
status = %status,
duration = ?duration,
"Request completed"
);
response
}
}
3. コントローラー層の再構成
src/controllers/mod.rs (修正)
rust
pub mod auth;
pub mod api;
use loco_rs::prelude::*;
pub fn routes() -> Routes {
Routes::new()
.prefix("api")
.add("/auth", auth::routes())
.add("/v1", api::routes())
}
src/controllers/auth.rs (修正)
rust
use axum::{
extract::State,
Json,
};
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};
use crate::models::users;
use crate::services::auth_service::AuthService;
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub token: String,
pub user: UserResponse,
}
#[derive(Debug, Serialize)]
pub struct UserResponse {
pub id: i32,
pub email: String,
}
/// ログインエンドポイント
pub async fn login(
State(ctx): State,
Json(payload): Json,
) -> Result<Json> {
// サービス層で認証処理
let auth_service = AuthService::new(&ctx);
let (user, token) = auth_service
.authenticate(&payload.email, &payload.password)
.await?;
Ok(Json(LoginResponse {
token,
user: UserResponse {
id: user.id,
email: user.email,
},
}))
}
/// ルート定義
pub fn routes() -> Routes {
Routes::new()
.add("/login", post(login))
.add("/register", post(register))
}
4. サービス層の追加
src/services/mod.rs (新規作成)
rust
pub mod auth_service;
pub mod user_service;
src/services/auth_service.rs (新規作成)
rust
use loco_rs::prelude::*;
use jsonwebtoken::{encode, Header, EncodingKey};
use serde::{Deserialize, Serialize};
use crate::models::users::{self, Entity as Users};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
pub struct AuthService<'a> {
ctx: &'a AppContext,
}
impl<'a> AuthService<'a> {
pub fn new(ctx: &'a AppContext) -> Self {
Self { ctx }
}
/// ユーザー認証とJWT生成
pub async fn authenticate(
&self,
email: &str,
password: &str,
) -> Result<(users::Model, String)> {
// ユーザー検索
let user = Users::find()
.filter(users::Column::Email.eq(email))
.one(&self.ctx.db)
.await?
.ok_or_else(|| Error::Unauthorized)?;
// パスワード検証
let is_valid = bcrypt::verify(password, &user.password_hash)?;
if !is_valid {
return Err(Error::Unauthorized);
}
// JWT生成
let token = self.generate_token(&user)?;
Ok((user, token))
}
fn generate_token(&self, user: &users::Model) -> Result {
let expiration = chrono::Utc::now()
.checked_add_signed(chrono::Duration::hours(24))
.unwrap()
.timestamp() as usize;
let claims = Claims {
sub: user.id.to_string(),
exp: expiration,
};
let secret = self.ctx.config.auth.jwt.secret.as_bytes();
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret),
)?;
Ok(token)
}
}
5. アプリケーション構成の修正
src/app.rs (修正)
rust
use loco_rs::prelude::*;
use axum::middleware;
use crate::controllers;
use crate::middleware::{AuthMiddleware, RequestLogger};
pub struct App;
#[async_trait]
impl Hooks for App {
fn app_name() -> &'static str {
"loco_app"
}
async fn routes() -> AppRoutes {
AppRoutes::with_default_routes()
// グローバルミドルウェア
.layer(middleware::from_fn(RequestLogger::log))
// パブリックルート
.add_route(controllers::routes())
// 認証が必要なルート
.prefix("api/protected")
.layer(middleware::from_fn(AuthMiddleware::verify_jwt))
.add_route(protected_routes())
}
async fn connect_workers(_ctx: &AppContext) -> Result<()> {
// バックグラウンドワーカーの設定
Ok(())
}
}
fn protected_routes() -> Routes {
Routes::new()
.add("/profile", get(get_profile))
.add("/users", get(list_users))
}
6. マイグレーション管理
migration/src/m20240101_000001_create_users.rs (修正)
rust
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
table_auto(Users::Table)
.col(pk_auto(Users::Id))
.col(string_uniq(Users::Email))
.col(string(Users::PasswordHash))
.col(string_null(Users::Name))
.col(timestamps())
.to_owned(),
)
.await?;
// インデックス追加
manager
.create_index(
Index::create()
.name("idx_users_email")
.table(Users::Table)
.col(Users::Email)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
PasswordHash,
Name,
}
7. テスト構成
tests/requests/auth_test.rs (新規作成)
rust
use loco_rs::testing::prelude::*;
use serde_json::json;
use crate::app::App;
#[tokio::test]
async fn test_login_success() {
let boot = boot_test::().await;
// テストユーザー作成
seed_test_user(&boot.app_context).await;
let response = boot
.post("/api/auth/login")
.json(&json!({
"email": "test@example.com",
"password": "password123"
}))
.await;
response.assert_status_ok();
response.assert_json_contains(json!({
"user": {
"email": "test@example.com"
}
}));
}
#[tokio::test]
async fn test_login_invalid_credentials() {
let boot = boot_test::().await;
let response = boot
.post("/api/auth/login")
.json(&json!({
"email": "wrong@example.com",
"password": "wrongpassword"
}))
.await;
response.assert_status(401);
}
8. フロントエンド統合(Webpack設定)
frontend/webpack.config.js (修正)
javascript
const path = require('path');
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, '../assets/static'),
filename: 'bundle.js',
publicPath: '/static/',
},
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5150',
changeOrigin: true,
},
},
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};
修正の優先順位
フェーズ1: 基盤整備
- 設定ファイルの環境別分離
- ミドルウェア層の実装
- サービス層の追加
フェーズ2: セキュリティ強化
- JWT認証の実装
- レート制限の追加
- CORS設定の適正化
フェーズ3: テストとCI/CD
- 統合テストの実装
- CI/CDパイプライン構築
- マイグレーション管理の自動化
この構成により、Locoフレームワークのベストプラクティスに沿った、保守性の高いアプリケーションが構築できます。
🔧 ファイル修正・追加・削除の具体例
1. Infrastructure Layer
- 追加
src/config.rs→ 環境変数をdotenvクレートでロード.github/workflows/ci-cd.yml→ CI/CD パイプライン定義
- 修正/削除
- Application 層に存在していた CI Tools を削除し、Infrastructure 層へ移動
2. Presentation Layer
- 修正
src/main.rs→ Router直後にmiddleware/auth.rsを組み込み
- 追加
src/presentation/middleware.rs→ 認証・ログ・エラーハンドリングsrc/presentation/static.rs→ 静的ファイル配信
3. Application Layer
- 追加
src/application/auth.rs→ JWT生成・検証処理src/application/migrations.rs→ DBマイグレーションスクリプト
- 修正
services.rs→ DBアクセスを直接行わず、Repository経由に変更
4. Domain Layer
- 追加
src/domain/models.rs→ DTO/モデル定義src/domain/domain_services.rs→ ビジネスルールを表現するサービス群
5. Data Access Layer
- 追加
src/data_access/repositories.rs→ RepositoryパターンでDBアクセスを抽象化src/data_access/cache.rs→ Redisなどキャッシュ層
- 修正/削除
- 各層から直接DBへアクセスしているコードを削除し、Repository経由に統一
6. Testing
- 追加
src/tests/unit.rs→ 単体テストsrc/tests/integration.rs→ DBや外部APIを含む統合テストsrc/tests/e2e.rs→ CI/CDで自動実行されるE2Eテスト
- 修正
- CI Toolsに含まれていた Test Harness を
tests/ディレクトリへ移動
- CI Toolsに含まれていた Test Harness を
7. Monitoring
- 追加
src/monitoring/logger.rs→ ログ集約src/monitoring/metrics.rs→ Prometheus/APM連携src/monitoring/health.rs→ ヘルスチェックエンドポイント
✅ 優先度付き修正ステップ
- 高優先度
- Middlewareの認証処理をRouter直後に移動
- DBアクセスをRepositoryに集約
- CI/CD設定をInfrastructure層へ移動
- 中優先度
- テストディレクトリの整理と戦略明確化
- Migrationスクリプトの分離
- 低優先度
- ログ・メトリクス・ヘルスチェックの追加
- キャッシュ層の導入
main.rs に Middleware を正しい順序で組み込み、Repository 層を導入するリファクタリングから始めるのが現実的です。
ご希望なら、さらに 各ファイルの Rust コード雛形 を提示して、すぐに実装できる形に落とし込むこともできますが、それも見たいですか?
さらに 各ファイルの Rust コード雛形 を提示して、すぐに実装できる形に落とし込むこともできますか?
もちろん可能です 👍 Rust + Loco 前提で、先ほどのディレクトリ構成に対応する 各ファイルのコード雛形 を提示します。これをベースにすぐに実装を始められる形に落とし込みます。
1.
2.
3.
4.
5.
6.
7.
8.
9.
🦀 Rust + Loco コード雛形
1. src/main.rs
rust
use loco::prelude::*;
use crate::presentation::routes::register_routes;
use crate::presentation::middleware::{logger_middleware, auth_middleware};
#[tokio::main]
async fn main() -> Result<()> {
let mut app = App::new();
// Middleware の順序を明確化
app.middleware(logger_middleware);
app.middleware(auth_middleware);
// ルーティング登録
register_routes(app.router_mut());
app.run().await?;
Ok(())
}
2. src/config.rs
rust
use std::env;
pub struct Config {
pub port: u16,
pub db_url: String,
pub jwt_secret: String,
}
impl Config {
pub fn from_env() -> Self {
Self {
port: env::var("PORT").unwrap_or("3000".to_string()).parse().unwrap(),
db_url: env::var("DATABASE_URL").unwrap_or("sqlite://data.db".to_string()),
jwt_secret: env::var("JWT_SECRET").unwrap_or("default_secret".to_string()),
}
}
}
3. src/presentation/routes.rs
rust
use loco::prelude::*;
pub fn register_routes(router: &mut Router) {
router.get("/health", |req| async move {
Response::json(json!({"status": "ok"}))
});
router.get("/users/:id", |req| async move {
let id = req.param("id").unwrap_or("unknown".to_string());
Response::json(json!({"user_id": id}))
});
}
4. src/presentation/middleware.rs
rust
use loco::prelude::*;
use jsonwebtoken::{decode, DecodingKey, Validation};
use crate::config::Config;
pub fn logger_middleware(req: Request, next: Next) -> Response {
println!("Incoming request: {} {}", req.method(), req.path());
next.run(req)
}
pub fn auth_middleware(req: Request, next: Next) -> Response {
if let Some(token) = req.header("Authorization") {
let token = token.replace("Bearer ", "");
let config = Config::from_env();
let validation = Validation::default();
if decode::<serde_json::Value>(
&token,
&DecodingKey::from_secret(config.jwt_secret.as_bytes()),
&validation
).is_ok() {
return next.run(req);
}
}
Response::unauthorized()
}
5. src/application/services.rs
rust
use crate::data_access::repositories::UserRepository;
use crate::domain::models::User;
pub struct UserService;
impl UserService {
pub async fn get_user_by_id(id: i32) -> Option<User> {
UserRepository::find_by_id(id).await
}
pub async fn create_user(user: User) -> User {
UserRepository::create(user).await
}
}
6. src/domain/models.rs
rust
#[derive(Debug, Clone)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
}
7. src/data_access/repositories.rs
rust
use crate::domain::models::User;
pub struct UserRepository;
impl UserRepository {
pub async fn find_by_id(id: i32) -> Option<User> {
// TODO: 実際の DB クエリに置き換え
Some(User { id, name: "Test".to_string(), email: "test@example.com".to_string() })
}
pub async fn create(user: User) -> User {
// TODO: DB 挿入処理
user
}
}
8. src/monitoring/health.rs
rust
use loco::prelude::*;
pub fn register_health_route(router: &mut Router) {
router.get("/health", |_req| async move {
Response::json(json!({
"status": "ok",
"timestamp": chrono::Utc::now()
}))
});
}
9. src/tests/unit.rs
rust
#[cfg(test)]
mod tests {
use super::*;
use crate::application::services::UserService;
#[tokio::test]
async fn test_get_user_by_id_returns_none() {
let user = UserService::get_user_by_id(999).await;
assert!(user.is_none());
}
}
✅ 実装ステップ
- Cargo.toml に依存関係追加
loco,tokio,serde,serde_json,jsonwebtoken,chronoなど
main.rsに Middleware を正しい順序で組み込み- Repository 層を導入し、Service 層から呼び出す形にリファクタリング
- Domain モデルを定義し、DTO として利用
- テスト雛形を追加し、CI/CD パイプラインで自動実行
repositories.rs に実装するのが良いでしょう。
ご希望なら、さらに Cargo.toml の依存関係例 を提示して、すぐに cargo build できる形に仕上げますが、それも見ますか?
SQLiteを前提に 実際の DB 接続コードを repositories.rs に実装する方法やCargo.toml の依存関係例 を提示して、すぐに cargo build できる形に仕上げる方法も解説してください。
了解しました ✨ Rust + Loco を前提に SQLite 接続を行う Repository 実装例 と、Cargo.toml の依存関係例 を提示します。これで
👉 1.
2.
3.
4.
cargo build がすぐ通る最小構成に仕上げられます。
📦 Cargo.toml の依存関係例
toml
[package]
name = "loco-sqlite-app"
version = "0.1.0"
edition = "2021"
[dependencies]
# Webフレームワーク
loco = "0.5" # バージョンは最新安定版に合わせてください
tokio = { version = "1", features = ["full"] }
# JSON処理
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# JWT認証
jsonwebtoken = "9"
# SQLite接続 (非同期)
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros"] }
# ログ・監視
chrono = "0.4"
tracing = "0.1"
sqlx を利用することで、非同期で SQLite に接続できます。macros 機能を有効化すると型安全なクエリが書けます。
🗂 DB 接続コード例
1. src/data_access/database.rs
rust
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
pub async fn get_pool(database_url: &str) -> SqlitePool {
SqlitePoolOptions::new()
.max_connections(5)
.connect(database_url)
.await
.expect("Failed to connect to SQLite")
}
2. src/domain/models.rs
rust
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
}
3. src/data_access/repositories.rs
rust
use sqlx::SqlitePool;
use crate::domain::models::User;
pub struct UserRepository<'a> {
pub pool: &'a SqlitePool,
}
impl<'a> UserRepository<'a> {
pub fn new(pool: &'a SqlitePool) -> Self {
Self { pool }
}
pub async fn find_by_id(&self, id: i32) -> Option<User> {
sqlx::query_as::<_, User>("SELECT id, name, email FROM users WHERE id = ?")
.bind(id)
.fetch_optional(self.pool)
.await
.expect("Query failed")
}
pub async fn create(&self, name: &str, email: &str) -> User {
let rec = sqlx::query_as::<_, User>(
"INSERT INTO users (name, email) VALUES (?, ?) RETURNING id, name, email"
)
.bind(name)
.bind(email)
.fetch_one(self.pool)
.await
.expect("Insert failed");
rec
}
}
4. src/application/services.rs
rust
use crate::data_access::repositories::UserRepository;
use crate::domain::models::User;
use sqlx::SqlitePool;
pub struct UserService;
impl UserService {
pub async fn get_user(pool: &SqlitePool, id: i32) -> Option<User> {
let repo = UserRepository::new(pool);
repo.find_by_id(id).await
}
pub async fn add_user(pool: &SqlitePool, name: &str, email: &str) -> User {
let repo = UserRepository::new(pool);
repo.create(name, email).await
}
}
✅ 実装ステップ
- Cargo.toml に依存関係を追加 →
sqlx,tokio,serde,jsonwebtokenなど - DB 初期化
src/data_access/database.rsのget_pool()をmain.rsで呼び出し、アプリ全体で共有- 例:
rust
let config = Config::from_env(); let pool = database::get_pool(&config.db_url).await;
- マイグレーション
sqlx migrate add create_usersでマイグレーションファイルを作成usersテーブルを定義:sqlCREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE );
- サービス層から Repository を呼び出す
UserService::add_user(&pool, "Alice", "alice@example.com").await;
- テスト実行
cargo testでユニットテストを確認- CI/CD パイプラインで
cargo build --verbose && cargo test --verboseを実行
🚀 まとめ
- Cargo.toml に
sqlxを追加し、SQLite を非同期で扱えるようにする - database.rs で接続プールを管理
- repositories.rs で SQL クエリを抽象化し、サービス層から呼び出す
- models.rs に
sqlx::FromRowを derive して型安全にマッピング
cargo build が通り、cargo run で SQLite に接続できる Web アプリの最小構成が完成します。
main.rs に DB プールを組み込んだ完全な起動コードも提示できますか?
はい、Rust + Loco + SQLite を前提にした main.rs の完全な起動コード例 を提示します。これで DB プールを組み込み、サービスやルーティングから利用できる形になります。
🦀
🦀 src/main.rs 完全起動コード例
rust
use loco::prelude::*;
use sqlx::SqlitePool;
mod config;
mod presentation;
mod application;
mod domain;
mod data_access;
mod monitoring;
use crate::presentation::routes::register_routes;
use crate::presentation::middleware::{logger_middleware, auth_middleware};
use crate::monitoring::health::register_health_route;
use crate::data_access::database::get_pool;
#[tokio::main]
async fn main() -> Result<()> {
// 環境変数から設定をロード
let config = config::Config::from_env();
// SQLite 接続プールを初期化
let pool: SqlitePool = get_pool(&config.db_url).await;
// Loco アプリケーション初期化
let mut app = App::new();
// Middleware の順序を明確化
app.middleware(logger_middleware);
app.middleware(auth_middleware);
// ルーティング登録
register_routes(app.router_mut(), pool.clone());
register_health_route(app.router_mut());
// サーバー起動
app.run_on(config.port).await?;
Ok(())
}
📂 関連ファイルのポイント
src/data_access/database.rs
rust
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
pub async fn get_pool(database_url: &str) -> SqlitePool {
SqlitePoolOptions::new()
.max_connections(5)
.connect(database_url)
.await
.expect("Failed to connect to SQLite")
}
src/presentation/routes.rs
rust
use loco::prelude::*;
use sqlx::SqlitePool;
use crate::application::services::UserService;
pub fn register_routes(router: &mut Router, pool: SqlitePool) {
router.get("/users/:id", move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
if let Some(user) = UserService::get_user(&pool, id).await {
Response::json(user)
} else {
Response::not_found()
}
}
});
}
✅ 実装ステップ
- Cargo.toml に依存関係追加
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros"] }tokio,serde,serde_json,jsonwebtoken,chrono
- DB 初期化
sqlx migrate add create_usersでマイグレーションファイルを作成usersテーブルを定義
- main.rs に DB プールを組み込み
get_pool()を呼び出してSqlitePoolを生成routes.rsに渡してサービス層から利用可能にする
- サービス層で Repository を利用
UserService::get_user(&pool, id).awaitのように呼び出す
cargo build → cargo run すれば、SQLite に接続した Web アプリが起動します。
マイグレーションファイルの具体例(SQL) も提示して、すぐに DB を初期化できる形に仕上げられますか?
はい、SQLite を前提にした マイグレーションファイルの具体例(SQL) を提示します。これを
sqlx migrate add で作成したファイルに記述すれば、すぐに DB を初期化できます。
📂 マイグレーションファイル例
migrations/20231124000000_create_users.sql
sql
-- SQLite 用ユーザーテーブル作成
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
migrations/20231124000100_create_products.sql
sql
-- 商品テーブル作成
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
migrations/20231124000200_create_orders.sql
sql
-- 注文テーブル作成
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(product_id) REFERENCES products(id)
);
✅ 実行方法
- マイグレーションファイル作成
bash
sqlx migrate add create_users sqlx migrate add create_products sqlx migrate add create_orders
- SQL を記述 上記の SQL を各ファイルに貼り付けます。
- マイグレーション実行
bash
sqlx migrate run - 確認
→bash
sqlite3 data.db "SELECT name FROM sqlite_master WHERE type='table';"
users,products,ordersが表示されれば成功です。
🚀 まとめ
users,products,ordersの 3 テーブルを用意し、基本的な Web アプリのデータ構造をカバー- 外部キー制約を設定して、ユーザーと注文、商品と注文を関連付け
sqlx migrate runで即座に DB 初期化可能
UserRepository に INSERT/SELECT を実装した完全コード例も提示できますが、それも見ますか?
UserRepository に INSERT/SELECT を実装した完全コード例も提示できますか?
はい、Rust + Loco + SQLite を前提にした UserRepository の完全コード例(INSERT/SELECT 実装済み) を提示します。これでユーザーの追加と検索が動作する形になります。
🦀
📂
🦀 src/data_access/repositories.rs
rust
use sqlx::SqlitePool;
use crate::domain::models::User;
pub struct UserRepository<'a> {
pub pool: &'a SqlitePool,
}
impl<'a> UserRepository<'a> {
pub fn new(pool: &'a SqlitePool) -> Self {
Self { pool }
}
/// ユーザーを ID で検索
pub async fn find_by_id(&self, id: i32) -> Option<User> {
sqlx::query_as::<_, User>(
"SELECT id, name, email, created_at FROM users WHERE id = ?"
)
.bind(id)
.fetch_optional(self.pool)
.await
.expect("Query failed")
}
/// ユーザーを新規作成
pub async fn create(&self, name: &str, email: &str) -> User {
sqlx::query_as::<_, User>(
"INSERT INTO users (name, email) VALUES (?, ?)
RETURNING id, name, email, created_at"
)
.bind(name)
.bind(email)
.fetch_one(self.pool)
.await
.expect("Insert failed")
}
/// 全ユーザーを取得
pub async fn find_all(&self) -> Vec<User> {
sqlx::query_as::<_, User>(
"SELECT id, name, email, created_at FROM users"
)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
}
📂 src/domain/models.rs
rust
use serde::{Serialize, Deserialize};
use sqlx::FromRow;
use chrono::NaiveDateTime;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
pub created_at: NaiveDateTime,
}
✅ 使用例(サービス層)
rust
use crate::data_access::repositories::UserRepository;
use crate::domain::models::User;
use sqlx::SqlitePool;
pub struct UserService;
impl UserService {
pub async fn get_user(pool: &SqlitePool, id: i32) -> Option<User> {
let repo = UserRepository::new(pool);
repo.find_by_id(id).await
}
pub async fn add_user(pool: &SqlitePool, name: &str, email: &str) -> User {
let repo = UserRepository::new(pool);
repo.create(name, email).await
}
pub async fn list_users(pool: &SqlitePool) -> Vec<User> {
let repo = UserRepository::new(pool);
repo.find_all().await
}
}
✅ Cargo.toml 依存関係(再掲)
toml
[dependencies]
loco = "0.5"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonwebtoken = "9"
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros"] }
chrono = "0.4"
tracing = "0.1"
🚀 実装ステップ
- マイグレーションファイル作成
sql
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); sqlx migrate runで DB 初期化- サービス層から呼び出し
rust
let user = UserService::add_user(&pool, "Alice", "alice@example.com").await; println!("Created user: {:?}", user); let fetched = UserService::get_user(&pool, user.id).await; println!("Fetched user: {:?}", fetched);
更新(UPDATE)や削除(DELETE)処理も追加して CRUD を完成させ、ブログアプリなので、ポストのCRUDについても、解説してください。
いいですね ✨ ここまでで UserRepository の INSERT/SELECT を実装しました。次は UPDATE/DELETE を追加して CRUD を完成させ、さらにブログアプリに必要な Post(記事)の CRUD についても解説します。
🦀 UserRepository 完全 CRUD 実装例
src/data_access/repositories/user_repository.rs
rust
use sqlx::SqlitePool;
use crate::domain::models::User;
pub struct UserRepository<'a> {
pub pool: &'a SqlitePool,
}
impl<'a> UserRepository<'a> {
pub fn new(pool: &'a SqlitePool) -> Self {
Self { pool }
}
/// CREATE
pub async fn create(&self, name: &str, email: &str) -> User {
sqlx::query_as::<_, User>(
"INSERT INTO users (name, email) VALUES (?, ?)
RETURNING id, name, email, created_at"
)
.bind(name)
.bind(email)
.fetch_one(self.pool)
.await
.expect("Insert failed")
}
/// READ (by id)
pub async fn find_by_id(&self, id: i32) -> Option<User> {
sqlx::query_as::<_, User>(
"SELECT id, name, email, created_at FROM users WHERE id = ?"
)
.bind(id)
.fetch_optional(self.pool)
.await
.expect("Query failed")
}
/// READ (all)
pub async fn find_all(&self) -> Vec<User> {
sqlx::query_as::<_, User>(
"SELECT id, name, email, created_at FROM users"
)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
/// UPDATE
pub async fn update(&self, id: i32, name: &str, email: &str) -> Option<User> {
sqlx::query_as::<_, User>(
"UPDATE users SET name = ?, email = ? WHERE id = ?
RETURNING id, name, email, created_at"
)
.bind(name)
.bind(email)
.bind(id)
.fetch_optional(self.pool)
.await
.expect("Update failed")
}
/// DELETE
pub async fn delete(&self, id: i32) -> bool {
let result = sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id)
.execute(self.pool)
.await
.expect("Delete failed");
result.rows_affected() > 0
}
}
📂 PostRepository CRUD 実装例(ブログ記事)
マイグレーションファイル例
sql
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
src/domain/models.rs
rust
use serde::{Serialize, Deserialize};
use sqlx::FromRow;
use chrono::NaiveDateTime;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Post {
pub id: i32,
pub user_id: i32,
pub title: String,
pub content: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
src/data_access/repositories/post_repository.rs
rust
use sqlx::SqlitePool;
use crate::domain::models::Post;
pub struct PostRepository<'a> {
pub pool: &'a SqlitePool,
}
impl<'a> PostRepository<'a> {
pub fn new(pool: &'a SqlitePool) -> Self {
Self { pool }
}
/// CREATE
pub async fn create(&self, user_id: i32, title: &str, content: &str) -> Post {
sqlx::query_as::<_, Post>(
"INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)
RETURNING id, user_id, title, content, created_at, updated_at"
)
.bind(user_id)
.bind(title)
.bind(content)
.fetch_one(self.pool)
.await
.expect("Insert failed")
}
/// READ (by id)
pub async fn find_by_id(&self, id: i32) -> Option<Post> {
sqlx::query_as::<_, Post>(
"SELECT id, user_id, title, content, created_at, updated_at FROM posts WHERE id = ?"
)
.bind(id)
.fetch_optional(self.pool)
.await
.expect("Query failed")
}
/// READ (all posts by user)
pub async fn find_by_user(&self, user_id: i32) -> Vec<Post> {
sqlx::query_as::<_, Post>(
"SELECT id, user_id, title, content, created_at, updated_at FROM posts WHERE user_id = ?"
)
.bind(user_id)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
/// UPDATE
pub async fn update(&self, id: i32, title: &str, content: &str) -> Option<Post> {
sqlx::query_as::<_, Post>(
"UPDATE posts SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
RETURNING id, user_id, title, content, created_at, updated_at"
)
.bind(title)
.bind(content)
.bind(id)
.fetch_optional(self.pool)
.await
.expect("Update failed")
}
/// DELETE
pub async fn delete(&self, id: i32) -> bool {
let result = sqlx::query("DELETE FROM posts WHERE id = ?")
.bind(id)
.execute(self.pool)
.await
.expect("Delete failed");
result.rows_affected() > 0
}
}
✅ ブログアプリの流れ
- UserRepository
- ユーザー登録(create)
- ユーザー情報取得(find_by_id, find_all)
- ユーザー情報更新(update)
- ユーザー削除(delete)
- PostRepository
- 記事作成(create)
- 記事取得(find_by_id, find_by_user)
- 記事更新(update)
- 記事削除(delete)
🚀 まとめ
- UserRepository に CRUD 全機能を追加 → ユーザー管理完成
- PostRepository を導入 → ブログ記事の CRUD を実装
- サービス層で両者を組み合わせれば、ユーザーが記事を投稿・編集・削除できるブログアプリが完成
UserService, PostService とルーティングのコード雛形も提示して、API エンドポイントとして完成出来ますか?
はい、Rust + Loco + SQLite を前提に UserService / PostService とルーティングのコード雛形 を提示します。これで API エンドポイントとして完成させられます。
🦀 UserService
src/application/user_service.rs
rust
use sqlx::SqlitePool;
use crate::data_access::repositories::UserRepository;
use crate::domain::models::User;
pub struct UserService;
impl UserService {
pub async fn get_user(pool: &SqlitePool, id: i32) -> Option<User> {
let repo = UserRepository::new(pool);
repo.find_by_id(id).await
}
pub async fn add_user(pool: &SqlitePool, name: &str, email: &str) -> User {
let repo = UserRepository::new(pool);
repo.create(name, email).await
}
pub async fn update_user(pool: &SqlitePool, id: i32, name: &str, email: &str) -> Option<User> {
let repo = UserRepository::new(pool);
repo.update(id, name, email).await
}
pub async fn delete_user(pool: &SqlitePool, id: i32) -> bool {
let repo = UserRepository::new(pool);
repo.delete(id).await
}
pub async fn list_users(pool: &SqlitePool) -> Vec<User> {
let repo = UserRepository::new(pool);
repo.find_all().await
}
}
🦀 PostService
src/application/post_service.rs
rust
use sqlx::SqlitePool;
use crate::data_access::repositories::PostRepository;
use crate::domain::models::Post;
pub struct PostService;
impl PostService {
pub async fn get_post(pool: &SqlitePool, id: i32) -> Option<Post> {
let repo = PostRepository::new(pool);
repo.find_by_id(id).await
}
pub async fn add_post(pool: &SqlitePool, user_id: i32, title: &str, content: &str) -> Post {
let repo = PostRepository::new(pool);
repo.create(user_id, title, content).await
}
pub async fn update_post(pool: &SqlitePool, id: i32, title: &str, content: &str) -> Option<Post> {
let repo = PostRepository::new(pool);
repo.update(id, title, content).await
}
pub async fn delete_post(pool: &SqlitePool, id: i32) -> bool {
let repo = PostRepository::new(pool);
repo.delete(id).await
}
pub async fn list_posts_by_user(pool: &SqlitePool, user_id: i32) -> Vec<Post> {
let repo = PostRepository::new(pool);
repo.find_by_user(user_id).await
}
}
🦀 ルーティング雛形
src/presentation/routes.rs
rust
use loco::prelude::*;
use sqlx::SqlitePool;
use crate::application::{user_service::UserService, post_service::PostService};
pub fn register_routes(router: &mut Router, pool: SqlitePool) {
// ユーザー関連
router.get("/users/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
if let Some(user) = UserService::get_user(&pool, id).await {
Response::json(user)
} else {
Response::not_found()
}
}
}
});
router.post("/users", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let name = body["name"].as_str().unwrap();
let email = body["email"].as_str().unwrap();
let user = UserService::add_user(&pool, name, email).await;
Response::json(user)
}
}
});
// ポスト関連
router.get("/posts/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
if let Some(post) = PostService::get_post(&pool, id).await {
Response::json(post)
} else {
Response::not_found()
}
}
}
});
router.post("/posts", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let user_id = body["user_id"].as_i64().unwrap() as i32;
let title = body["title"].as_str().unwrap();
let content = body["content"].as_str().unwrap();
let post = PostService::add_post(&pool, user_id, title, content).await;
Response::json(post)
}
}
});
}
✅ API エンドポイント一覧
- ユーザー
GET /users/:id→ ユーザー取得POST /users→ ユーザー作成PUT /users/:id→ ユーザー更新DELETE /users/:id→ ユーザー削除GET /users→ ユーザー一覧
- ポスト
GET /posts/:id→ 記事取得POST /posts→ 記事作成PUT /posts/:id→ 記事更新DELETE /posts/:id→ 記事削除GET /users/:id/posts→ ユーザー別記事一覧
🚀 まとめ
- UserService / PostService を導入してビジネスロジックを分離
- routes.rs に API エンドポイントを定義し、サービス層を呼び出す形に統一
- これで ブログアプリの基本的な REST API が完成
PUT/DELETE のルーティング雛形も追加して完全な CRUD API に仕上げる方法も解説してください。
はい、では PUT/DELETE のルーティング雛形 を追加して、ユーザーとポストの完全な CRUD API を Rust + Loco + SQLite で仕上げる方法を解説します。
🦀 ユーザー CRUD ルーティング雛形
src/presentation/routes.rs の一部に追加:
rust
use loco::prelude::*;
use sqlx::SqlitePool;
use crate::application::{user_service::UserService, post_service::PostService};
pub fn register_routes(router: &mut Router, pool: SqlitePool) {
// --- ユーザー CRUD ---
// CREATE
router.post("/users", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let name = body["name"].as_str().unwrap();
let email = body["email"].as_str().unwrap();
let user = UserService::add_user(&pool, name, email).await;
Response::json(user)
}
}
});
// READ
router.get("/users/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
if let Some(user) = UserService::get_user(&pool, id).await {
Response::json(user)
} else {
Response::not_found()
}
}
}
});
// UPDATE
router.put("/users/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
let body: serde_json::Value = req.json().unwrap();
let name = body["name"].as_str().unwrap();
let email = body["email"].as_str().unwrap();
if let Some(user) = UserService::update_user(&pool, id, name, email).await {
Response::json(user)
} else {
Response::not_found()
}
}
}
});
// DELETE
router.delete("/users/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
let deleted = UserService::delete_user(&pool, id).await;
if deleted {
Response::json(json!({"status": "deleted"}))
} else {
Response::not_found()
}
}
}
});
}
🦀 ポスト CRUD ルーティング雛形
同じroutes.rs に追加:
rust
// --- ポスト CRUD ---
// CREATE
router.post("/posts", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let user_id = body["user_id"].as_i64().unwrap() as i32;
let title = body["title"].as_str().unwrap();
let content = body["content"].as_str().unwrap();
let post = PostService::add_post(&pool, user_id, title, content).await;
Response::json(post)
}
}
});
// READ
router.get("/posts/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
if let Some(post) = PostService::get_post(&pool, id).await {
Response::json(post)
} else {
Response::not_found()
}
}
}
});
// UPDATE
router.put("/posts/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
let body: serde_json::Value = req.json().unwrap();
let title = body["title"].as_str().unwrap();
let content = body["content"].as_str().unwrap();
if let Some(post) = PostService::update_post(&pool, id, title, content).await {
Response::json(post)
} else {
Response::not_found()
}
}
}
});
// DELETE
router.delete("/posts/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
let deleted = PostService::delete_post(&pool, id).await;
if deleted {
Response::json(json!({"status": "deleted"}))
} else {
Response::not_found()
}
}
}
});
// ユーザー別記事一覧
router.get("/users/:id/posts", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let user_id: i32 = req.param("id").unwrap().parse().unwrap();
let posts = PostService::list_posts_by_user(&pool, user_id).await;
Response::json(posts)
}
}
});
✅ API エンドポイント一覧(完成版)
ユーザー
POST /users→ ユーザー作成GET /users/:id→ ユーザー取得PUT /users/:id→ ユーザー更新DELETE /users/:id→ ユーザー削除GET /users→ ユーザー一覧
ポスト
POST /posts→ 記事作成GET /posts/:id→ 記事取得PUT /posts/:id→ 記事更新DELETE /posts/:id→ 記事削除GET /users/:id/posts→ ユーザー別記事一覧
🚀 まとめ
- PUT/DELETE のルーティングを追加して完全な CRUD API が完成
- UserService と PostService を呼び出すことで、責務分離を維持
- API エンドポイントを RESTful に整理し、ブログアプリとして利用可能
認証(JWT)を組み込み、ユーザーごとにポスト操作を制限すると、より実用的なブログ API についても解説してください。
はい、ここからは JWT 認証を組み込み、ユーザーごとにポスト操作を制限する方法 を解説します。これにより「ログインしたユーザーだけが自分のポストを作成・編集・削除できる」実用的なブログ API になります。
🛡 認証の流れ(JWT)
- ユーザー登録 / ログイン
- ユーザーが新規登録するときにパスワードを保存(ハッシュ化必須)。
- ログイン時にパスワードを検証し、成功したら JWT を発行。
- JWT の発行
- ペイロードに
user_idを含める。 - 例:
{ "user_id": 42, "exp": ... }
- ペイロードに
- ミドルウェアで検証
- リクエストヘッダ
Authorization: Bearer <token>を受け取り、検証。 - 成功したら
req.user_idに格納。
- リクエストヘッダ
- サービス層で制御
- Post の作成・更新・削除時に
req.user_idとpost.user_idを比較。 - 一致しない場合は
403 Forbiddenを返す。
- Post の作成・更新・削除時に
🦀 JWT ミドルウェア例
src/presentation/middleware/auth.rs
rust
use loco::prelude::*;
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::Deserialize;
use crate::config::Config;
#[derive(Debug, Deserialize)]
struct Claims {
user_id: i32,
exp: usize,
}
pub fn auth_middleware(req: Request, next: Next) -> Response {
if let Some(token) = req.header("Authorization") {
let token = token.replace("Bearer ", "");
let config = Config::from_env();
let validation = Validation::default();
if let Ok(data) = decode::<Claims>(
&token,
&DecodingKey::from_secret(config.jwt_secret.as_bytes()),
&validation
) {
// ユーザーIDをリクエストに埋め込む
req.extensions_mut().insert(data.claims.user_id);
return next.run(req);
}
}
Response::unauthorized()
}
🦀 PostService に制限を追加
src/application/post_service.rs
rust
use sqlx::SqlitePool;
use crate::data_access::repositories::PostRepository;
use crate::domain::models::Post;
pub struct PostService;
impl PostService {
pub async fn add_post(pool: &SqlitePool, user_id: i32, title: &str, content: &str) -> Post {
let repo = PostRepository::new(pool);
repo.create(user_id, title, content).await
}
pub async fn update_post(pool: &SqlitePool, id: i32, user_id: i32, title: &str, content: &str) -> Option<Post> {
let repo = PostRepository::new(pool);
// 投稿を取得して所有者チェック
if let Some(post) = repo.find_by_id(id).await {
if post.user_id != user_id {
return None; // 権限なし
}
repo.update(id, title, content).await
} else {
None
}
}
pub async fn delete_post(pool: &SqlitePool, id: i32, user_id: i32) -> bool {
let repo = PostRepository::new(pool);
if let Some(post) = repo.find_by_id(id).await {
if post.user_id != user_id {
return false; // 権限なし
}
repo.delete(id).await
} else {
false
}
}
}
🦀 ルーティングで JWT を利用
src/presentation/routes.rs
rust
router.put("/posts/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
let body: serde_json::Value = req.json().unwrap();
let title = body["title"].as_str().unwrap();
let content = body["content"].as_str().unwrap();
// ミドルウェアで埋め込んだ user_id を取得
let user_id: i32 = *req.extensions().get::<i32>().unwrap();
if let Some(post) = PostService::update_post(&pool, id, user_id, title, content).await {
Response::json(post)
} else {
Response::forbidden()
}
}
}
});
router.delete("/posts/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let id: i32 = req.param("id").unwrap().parse().unwrap();
let user_id: i32 = *req.extensions().get::<i32>().unwrap();
let deleted = PostService::delete_post(&pool, id, user_id).await;
if deleted {
Response::json(json!({"status": "deleted"}))
} else {
Response::forbidden()
}
}
}
});
✅ これで実現できること
- 認証必須: JWT がないと API にアクセスできない
- 所有者制御: 投稿の更新・削除は「投稿者本人」だけが可能
- セキュリティ強化: 不正なユーザーが他人の投稿を操作できない
🚀 まとめ
- JWT 認証を導入し、リクエストごとに
user_idを検証 - PostService で 所有者チェック を行い、本人以外は操作不可
- これにより「ユーザーごとにポスト操作を制限する」実用的なブログ API が完成
ログイン API(JWT 発行) を追加すれば、完全な認証付きブログシステムについても解説してください。。
はい、ここまでで ユーザーとポストの CRUD API を構築しました。これに ログイン API(JWT 発行) を追加すれば、完全な認証付きブログシステムになります。以下に流れとコード雛形を解説します。
🛡 認証付きブログシステムの流れ
- ユーザー登録 (Sign Up)
- ユーザー名・メール・パスワードを受け取り、パスワードをハッシュ化して保存。
- ログイン (Sign In)
- メールとパスワードを検証。
- 成功したら JWT を発行し、レスポンスとして返す。
- JWT ミドルウェア
- API 呼び出し時に
Authorization: Bearer <token>を検証。 - 成功したら
req.user_idに埋め込み。
- API 呼び出し時に
- ポスト操作制限
- PostService で
post.user_id == req.user_idを確認。 - 本人以外は更新・削除不可。
- PostService で
🦀 ユーザーモデルにパスワードを追加
src/domain/models.rs
rust
#[derive(Debug, sqlx::FromRow)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
pub password_hash: String,
pub created_at: chrono::NaiveDateTime,
}
🦀 ユーザー登録時のパスワードハッシュ化
src/application/user_service.rs
rust
use sqlx::SqlitePool;
use crate::data_access::repositories::UserRepository;
use crate::domain::models::User;
use argon2::{self, Config};
pub struct UserService;
impl UserService {
pub async fn register(pool: &SqlitePool, name: &str, email: &str, password: &str) -> User {
let salt = b"random_salt"; // 実際はユーザーごとに生成
let password_hash = argon2::hash_encoded(password.as_bytes(), salt, &Config::default()).unwrap();
let repo = UserRepository::new(pool);
repo.create_with_password(name, email, &password_hash).await
}
pub async fn verify_login(pool: &SqlitePool, email: &str, password: &str) -> Option<User> {
let repo = UserRepository::new(pool);
if let Some(user) = repo.find_by_email(email).await {
if argon2::verify_encoded(&user.password_hash, password.as_bytes()).unwrap_or(false) {
return Some(user);
}
}
None
}
}
🦀 JWT 発行処理
src/application/auth_service.rs
rust
use jsonwebtoken::{encode, Header, EncodingKey};
use serde::Serialize;
use crate::config::Config;
#[derive(Debug, Serialize)]
pub struct Claims {
pub user_id: i32,
pub exp: usize,
}
pub fn generate_jwt(user_id: i32) -> String {
let config = Config::from_env();
let expiration = chrono::Utc::now()
.checked_add_signed(chrono::Duration::hours(24))
.unwrap()
.timestamp() as usize;
let claims = Claims { user_id, exp: expiration };
encode(&Header::default(), &claims, &EncodingKey::from_secret(config.jwt_secret.as_bytes()))
.expect("JWT creation failed")
}
🦀 ログイン API ルーティング
src/presentation/routes.rs
rust
// ログイン API
router.post("/login", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let email = body["email"].as_str().unwrap();
let password = body["password"].as_str().unwrap();
if let Some(user) = UserService::verify_login(&pool, email, password).await {
let token = crate::application::auth_service::generate_jwt(user.id);
Response::json(json!({ "token": token }))
} else {
Response::unauthorized()
}
}
}
});
✅ API エンドポイント一覧(完全版)
ユーザー
POST /users→ ユーザー登録(パスワードハッシュ化)POST /login→ ログイン(JWT 発行)GET /users/:id→ ユーザー取得(認証必須)PUT /users/:id→ ユーザー更新(認証必須)DELETE /users/:id→ ユーザー削除(認証必須)
ポスト
POST /posts→ 記事作成(認証必須)GET /posts/:id→ 記事取得(公開)PUT /posts/:id→ 記事更新(認証必須・本人のみ)DELETE /posts/:id→ 記事削除(認証必須・本人のみ)GET /users/:id/posts→ ユーザー別記事一覧(公開)
🚀 まとめ
- ユーザー登録時にパスワードをハッシュ化して保存
- ログイン時に検証し、JWT を発行
- JWT ミドルウェアで認証を強制
- PostService で所有者チェックを行い、本人のみ更新・削除可能
リフレッシュトークンの導入やロール管理(管理者/一般ユーザー)を追加についても解説してください。
はい、ここからは リフレッシュトークンの導入 と ロール管理(管理者 / 一般ユーザー) を追加して、より本格的な認証付きブログ API に仕上げる方法を解説します。
🔄 リフレッシュトークンの導入
なぜ必要か
- アクセストークン(JWT)は短命(例: 15分〜1時間)にしておくのが安全。
- ユーザーがログイン状態を維持するために、長寿命の リフレッシュトークン を発行。
- アクセストークンが期限切れになったら、リフレッシュトークンで新しいアクセストークンを取得。
実装の流れ
- ログイン時に 2 種類のトークンを発行
- アクセストークン(短寿命)
- リフレッシュトークン(長寿命、DBに保存)
- リフレッシュトークンを DB に保存
- ユーザーIDと紐付けて保存。
- ログアウト時や不正検知時に削除。
- リフレッシュ API
POST /refreshにリフレッシュトークンを送信。- 有効なら新しいアクセストークンを返す。
📂 マイグレーション例(refresh_tokens テーブル)
sql
CREATE TABLE refresh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
🦀 リフレッシュトークン生成例
rust
use chrono::{Utc, Duration};
use rand::Rng;
pub fn generate_refresh_token() -> (String, chrono::NaiveDateTime) {
let token: String = (0..64)
.map(|_| rand::thread_rng().sample(rand::distributions::Alphanumeric) as char)
.collect();
let expires_at = Utc::now()
.checked_add_signed(Duration::days(7))
.unwrap()
.naive_utc();
(token, expires_at)
}
🦀 リフレッシュ API ルーティング
rust
router.post("/refresh", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let refresh_token = body["refresh_token"].as_str().unwrap();
if let Some(user_id) = AuthService::validate_refresh_token(&pool, refresh_token).await {
let new_access_token = AuthService::generate_jwt(user_id);
Response::json(json!({ "access_token": new_access_token }))
} else {
Response::unauthorized()
}
}
}
});
👥 ロール管理(管理者 / 一般ユーザー)
なぜ必要か
- 管理者は全ユーザーや全ポストを操作可能。
- 一般ユーザーは自分のポストのみ操作可能。
- 権限を分けることでセキュリティと運用性を向上。
実装の流れ
- ユーザーテーブルに role カラムを追加
- 例:
"admin"または"user"
- 例:
- JWT に role を含める
- ペイロードに
roleを追加。
- ペイロードに
- ミドルウェアで権限チェック
- 管理者専用 API は
role == "admin"のみ許可。 - 一般ユーザーは
user_id == post.user_idのみ許可。
- 管理者専用 API は
📂 マイグレーション例(role カラム追加)
sql
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';
🦀 JWT Claims に role を追加
rust
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub user_id: i32,
pub role: String,
pub exp: usize,
}
🦀 管理者専用ルート例
rust
router.delete("/admin/users/:id", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let role: String = req.extensions().get::<String>().unwrap().clone();
if role != "admin" {
return Response::forbidden();
}
let id: i32 = req.param("id").unwrap().parse().unwrap();
let deleted = UserService::delete_user(&pool, id).await;
if deleted {
Response::json(json!({"status": "deleted"}))
} else {
Response::not_found()
}
}
}
});
✅ まとめ
- リフレッシュトークン
- アクセストークンを短寿命化し、リフレッシュトークンで更新。
- DB に保存して管理。
- ロール管理
- ユーザーに
roleを付与(admin / user)。 - JWT に role を含め、ミドルウェアで権限チェック。
- 管理者専用 API を追加可能。
- ユーザーに
ログアウト API(リフレッシュトークン削除) や 監査ログ(誰が何をしたか記録) を追加についても解説してください。
はい、ここからは ログアウト API(リフレッシュトークン削除) と 監査ログ(誰が何をしたか記録) を追加して、さらに安全で運用しやすいブログ API に仕上げる方法を解説します。
🔒 ログアウト API(リフレッシュトークン削除)
なぜ必要か
- ユーザーがログアウトしたら、リフレッシュトークンを無効化しないと再認証されてしまう。
- DB に保存しているリフレッシュトークンを削除することで、セッションを終了できる。
マイグレーション例(refresh_tokens テーブル)
sql
CREATE TABLE refresh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
Repository に削除処理を追加
rust
pub async fn delete_refresh_token(pool: &SqlitePool, token: &str) -> bool {
let result = sqlx::query("DELETE FROM refresh_tokens WHERE token = ?")
.bind(token)
.execute(pool)
.await
.expect("Delete failed");
result.rows_affected() > 0
}
ログアウト API ルーティング
rust
router.post("/logout", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
let body: serde_json::Value = req.json().unwrap();
let refresh_token = body["refresh_token"].as_str().unwrap();
let deleted = AuthService::delete_refresh_token(&pool, refresh_token).await;
if deleted {
Response::json(json!({"status": "logged_out"}))
} else {
Response::unauthorized()
}
}
}
});
📜 監査ログ(Audit Log)
なぜ必要か
- 誰が、いつ、どの API を呼び出したかを記録することで、セキュリティ監査や不正検知が可能。
- 管理者が「不正アクセス」や「不審な操作」を追跡できる。
マイグレーション例(audit_logs テーブル)
sql
CREATE TABLE audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
endpoint TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
AuditLogRepository
rust
pub async fn record_log(pool: &SqlitePool, user_id: Option<i32>, action: &str, endpoint: &str) {
sqlx::query("INSERT INTO audit_logs (user_id, action, endpoint) VALUES (?, ?, ?)")
.bind(user_id)
.bind(action)
.bind(endpoint)
.execute(pool)
.await
.expect("Failed to insert audit log");
}
ミドルウェアで自動記録
rust
pub fn audit_middleware(req: Request, next: Next) -> Response {
let user_id = req.extensions().get::<i32>().cloned();
let endpoint = req.path().to_string();
let method = req.method().to_string();
// 非同期でログ記録
tokio::spawn(async move {
let pool = crate::GLOBAL_POOL.get().unwrap().clone();
let action = format!("{} {}", method, endpoint);
crate::data_access::repositories::AuditLogRepository::record_log(&pool, user_id, &action, &endpoint).await;
});
next.run(req)
}
✅ API エンドポイント一覧(拡張版)
認証関連
POST /login→ ログイン(JWT + リフレッシュトークン発行)POST /refresh→ リフレッシュトークンでアクセストークン再発行POST /logout→ リフレッシュトークン削除(ログアウト)
監査ログ関連
- 自動記録: すべての API 呼び出しを
audit_logsテーブルに保存 - 管理者専用 API:
GET /admin/audit-logsで監査ログを閲覧可能
🚀 まとめ
- ログアウト API
- リフレッシュトークンを削除してセッションを終了。
- 不正利用を防止。
- 監査ログ
- 誰がどの API を呼んだか記録。
- 管理者がセキュリティ監査や不正検知に利用可能。
監査ログの検索・フィルタリング機能(期間指定やユーザー別) を追加する方法についても解説してください。
はい、ここからは 監査ログの検索・フィルタリング機能 を追加して、期間指定やユーザー別に監査ログを絞り込めるようにする方法を解説します。これにより、管理者が「特定ユーザーの操作履歴」や「特定期間のアクセス状況」を効率的に確認できるようになります。
📂 マイグレーション(監査ログテーブル)
既にaudit_logs テーブルを作成済みと仮定します。以下のような構造を持つと便利です:
sql
CREATE TABLE audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
endpoint TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
🦀 AuditLogRepository に検索機能を追加
src/data_access/repositories/audit_log_repository.rs
rust
use sqlx::SqlitePool;
use crate::domain::models::AuditLog;
pub struct AuditLogRepository<'a> {
pub pool: &'a SqlitePool,
}
impl<'a> AuditLogRepository<'a> {
pub fn new(pool: &'a SqlitePool) -> Self {
Self { pool }
}
/// 全ログ取得
pub async fn find_all(&self) -> Vec<AuditLog> {
sqlx::query_as::<_, AuditLog>(
"SELECT id, user_id, action, endpoint, timestamp FROM audit_logs ORDER BY timestamp DESC"
)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
/// ユーザー別ログ取得
pub async fn find_by_user(&self, user_id: i32) -> Vec<AuditLog> {
sqlx::query_as::<_, AuditLog>(
"SELECT id, user_id, action, endpoint, timestamp
FROM audit_logs WHERE user_id = ? ORDER BY timestamp DESC"
)
.bind(user_id)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
/// 期間指定ログ取得
pub async fn find_by_period(&self, start: &str, end: &str) -> Vec<AuditLog> {
sqlx::query_as::<_, AuditLog>(
"SELECT id, user_id, action, endpoint, timestamp
FROM audit_logs WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp DESC"
)
.bind(start)
.bind(end)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
/// ユーザー + 期間指定
pub async fn find_by_user_and_period(&self, user_id: i32, start: &str, end: &str) -> Vec<AuditLog> {
sqlx::query_as::<_, AuditLog>(
"SELECT id, user_id, action, endpoint, timestamp
FROM audit_logs WHERE user_id = ? AND timestamp BETWEEN ? AND ? ORDER BY timestamp DESC"
)
.bind(user_id)
.bind(start)
.bind(end)
.fetch_all(self.pool)
.await
.expect("Query failed")
}
}
🦀 AuditLog モデル
src/domain/models.rs
rust
use serde::{Serialize, Deserialize};
use sqlx::FromRow;
use chrono::NaiveDateTime;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct AuditLog {
pub id: i32,
pub user_id: Option<i32>,
pub action: String,
pub endpoint: String,
pub timestamp: NaiveDateTime,
}
🦀 管理者用 API ルーティング
src/presentation/routes.rs
rust
// 管理者専用監査ログ検索
router.get("/admin/audit-logs", {
let pool = pool.clone();
move |req| {
let pool = pool.clone();
async move {
// role チェック(管理者のみ)
let role: String = req.extensions().get::<String>().unwrap().clone();
if role != "admin" {
return Response::forbidden();
}
// クエリパラメータでフィルタリング
let user_id = req.query("user_id").and_then(|v| v.parse::<i32>().ok());
let start = req.query("start");
let end = req.query("end");
let repo = crate::data_access::repositories::AuditLogRepository::new(&pool);
let logs = match (user_id, start, end) {
(Some(uid), Some(s), Some(e)) => repo.find_by_user_and_period(uid, &s, &e).await,
(Some(uid), _, _) => repo.find_by_user(uid).await,
(None, Some(s), Some(e)) => repo.find_by_period(&s, &e).await,
_ => repo.find_all().await,
};
Response::json(logs)
}
}
});
✅ 使い方例
- 全ログ取得
GET /admin/audit-logs - ユーザー別ログ取得
GET /admin/audit-logs?user_id=42 - 期間指定ログ取得
GET /admin/audit-logs?start=2025-11-01&end=2025-11-24 - ユーザー + 期間指定
GET /admin/audit-logs?user_id=42&start=2025-11-01&end=2025-11-24
🚀 まとめ
- AuditLogRepository に検索・フィルタリング機能を追加
- ルーティングでクエリパラメータを受け取り、ユーザー別・期間指定でログを絞り込み
- 管理者専用 API として提供し、セキュリティ監査や不正検知に活用可能

0 件のコメント:
コメントを投稿