playpark
ホーム会社概要サービスソリューションブログお知らせ気軽に相談する
playpark

あらゆる仕事を楽しむ

会社概要サービスソリューション気軽に相談する特定商取引法に基づく表記

© 2019-2026 合同会社playpark All Rights Reserved.

  1. ホーム
  2. ブログ
  3. 技術Tips
  4. Rocket FrameworkでRust APIサーバーを構築する - 実践アーキテクチャガイド
ブログ一覧に戻る
技術Tips

Rocket FrameworkでRust APIサーバーを構築する - 実践アーキテクチャガイド

RustのRocketフレームワークを使ったAPIサーバー構築の実践ガイド。SeaORM、Redis、クリーンアーキテクチャ、Cronジョブまで、本番運用を見据えた設計パターンを解説します。

2026年3月24日35分で読める
RustAPI開発バックエンドマイクロサービスDocker
Rocket FrameworkでRust APIサーバーを構築する - 実践アーキテクチャガイド

「Rustでバックエンド?学習コスト高すぎでしょ...」

その気持ち、わかります。所有権、ライフタイム、借用チェッカー...。「コンパイル通らない」と格闘した経験、ありませんか?

でも、一度コンパイルが通れば、ランタイムエラーで深夜に叩き起こされることがほぼない。この安心感、一度味わうと戻れません。

今回はyaoyorozチームで開発中のクリックカウンターアプリ「カゾエルくん」のバックエンド実装を題材に、Rocket Frameworkを使ったAPIサーバー構築を解説します。

なぜ「非生産的なアプリ」にRust?

yaoyorozは「なんかいつも不思議なものを作ってるチーム」として認知されることを目指すチームで、playpark LLCもメンバーとして参加しています。

カゾエルくんのコンセプトは「非生産的の極み」。クリックしてボタンを押したら数が増えるだけ。それ以上でもそれ以下でもない。

生産性と幸せは全然結びつかない。人間が幸せになる瞬間は、生産性の及ばないところにこそある。

子供と遊ぶ、旅行する、焚き火をする...どれも「非効率」なのに、そこにこそ愛着を感じる。だから「非生産的の極み」をあえて本気で作ってみようと。(哲学的だけど、技術スタックは真面目)

で、「簡単なテーマでRust勉強したい」という動機もあり、この組み合わせに。クリックカウンター程度なら学習コストの元が取れるという判断です。

この記事で学べること

  • Rocket Frameworkの基本構成とルーティング
  • SeaORMを使ったデータベース連携
  • Redisキャッシュとの統合パターン
  • クリーンアーキテクチャ(Handler → Service → Repository)
  • tokio::spawnを使った組み込みCronジョブ
  • Docker Composeでの開発環境構築

あわせて読みたい

【Rust】「非生産的」なWebアプリを作ってみた - Yew + Rocketフルスタック開発
技術Tips36分

【Rust】「非生産的」なWebアプリを作ってみた - Yew + Rocketフルスタック開発

フロントエンドもバックエンドも全部Rustで書いたクリックカウンターアプリの技術解説。Yew(WASM)+ Rocket + Redis + PostgreSQLの構成・API設計・Docker Compose環境構築まで、Rust統一フルスタック開発の実践ノウハウをコード付きで紹介します。

読む

前提条件

  • Rustの基本文法(所有権、トレイトで挫折しかけた経験があればなお良し)
  • REST APIの概念
  • Docker/PostgreSQLの基礎知識

あわせて読みたい

【OpenClaw Docker セキュリティ】CVE対策で学ぶコンテナハードニング実践 — cap_drop ALL から始める安全運用
技術Tips30分

【OpenClaw Docker セキュリティ】CVE対策で学ぶコンテナハードニング実践 — cap_drop ALL から始める安全運用

OpenClawをDocker化しCVE-2026対策を実施。cap_drop ALL・read_only・no-new-privilegesのハードニング設定と、AI開発ツールのコンテナセキュリティ設計を実コード付きで解説します。

読む

プロジェクト概要

今回構築するのは、セッションベースのクリックカウンターAPI。アーキテクチャは以下の通りです。

レイヤー技術役割
フレームワークRocket 0.5HTTPハンドリング
ORMSeaORMPostgreSQL操作
キャッシュdeadpool-redisRedis接続プール
非同期tokioCronジョブ実行

APIエンドポイント

POST   /session          # クリックをカウント
PUT    /session          # Redis → PostgreSQL同期
GET    /session_counter  # 集計データ取得
PUT    /status_history   # 週次ステータス更新
GET    /status_history   # ステータス履歴取得
GET    /health           # ヘルスチェック

たった6エンドポイント。シンプルですが、これだけで本番運用に必要なパターンは一通り学べます。(「非生産的」なアプリなのに、設計は生産的)

1. プロジェクト構成

app/backend/
├── src/
│   ├── main.rs              # エントリーポイント
│   ├── lib.rs               # モジュール定義
│   ├── routes.rs            # ルーティング
│   ├── config/              # 設定管理
│   ├── handlers/            # HTTPハンドラー
│   ├── services/            # ビジネスロジック
│   ├── repositories/        # データアクセス
│   ├── models/              # ドメインモデル
│   ├── cron/                # 定期実行タスク
│   └── http/                # HTTPユーティリティ
├── migration/               # DBマイグレーション
└── Cargo.toml

この構造はクリーンアーキテクチャに従っています。(「また設計パターンか...」と思った方、気持ちはわかります)

読み込み中...図を読み込み中

ただ、Rustの場合はこの分離がコンパイル時に強制されるので、「気づいたらFat Controller」みたいな事故が起きにくいんです。借用チェッカーに怒られながら書いた結果、自然とレイヤー分離されてた...なんてことも。

2. main.rs - エントリーポイント

実際のコードを見てみましょう。DIコンテナを使った依存性注入がポイントです。

use opt::config::AppConfig;
use opt::di::DiContainer;
use opt::routes::configure_routes;
use std::sync::Arc;

mod cron;
use cron::{spawn_status_history_cron_job, spawn_sync_cron_job};

#[macro_use]
extern crate rocket;

#[launch]
async fn rocket() -> _ {
    // Initialize logger
    env_logger::init();
    log::info!("Starting Kazoeru-kun application...");

    // Load configuration
    let app_config = AppConfig::from_env()
        .expect("Failed to load configuration");

    // Build dependency injection container
    let container = DiContainer::build(app_config)
        .await
        .expect("Failed to build dependency injection container");

    // Start cron jobs
    spawn_sync_cron_job(container.sync_service.clone());
    spawn_status_history_cron_job(
        container.status_service.clone(),
        Arc::new(container.config.clone())
    );

    // Get CORS configuration
    let cors = container.get_cors()
        .expect("Failed to configure CORS");

    // Build and configure Rocket application
    rocket::build()
        .manage(container.session_service.clone())
        .manage(container.status_service.clone())
        .manage(container.sync_service.clone())
        .manage(container.health_service.clone())
        .manage(container.session_repo.clone())
        .manage(container.counter_repo.clone())
        .manage(container.history_repo.clone())
        .manage(container.redis_pool.clone())
        .manage(container.db.clone())
        .manage(container.config.clone())
        .mount("/", configure_routes())
        .attach(cors)
}

ポイント:

  • DiContainer で依存性を一元管理(テスト時のモック差し替えが楽)
  • .manage() でサービスをRocket全体に注入
  • Cronジョブはサーバー起動時に tokio::spawn で開始

3. ハンドラー層 - 薄く保つ

ハンドラーはリクエスト/レスポンスの変換のみを担当します。ビジネスロジックは一切書かない。

use crate::http::cookie_utils::set_session_cookie;
use crate::models::response::{ErrorResponse, Response, SessionCount};
use crate::services::SessionService;
use rocket::{http::CookieJar, post, serde::json::Json, State};
use std::sync::Arc;

/// POST /session - セッションのクリックカウンタをインクリメント
#[post("/session")]
pub async fn create_session(
    session_service: &State<Arc<SessionService>>,
    cookies: &CookieJar<'_>,
) -> Result<Json<Response<SessionCount>>, Json<ErrorResponse>> {
    // Cookieからセッション取得
    let session_id = cookies.get("session").map(|cookie| cookie.value());

    // ビジネスロジックはサービス層に委譲
    match session_service.increment_session(session_id).await {
        Ok(session_response) => {
            // 新規セッションの場合はクッキーを設定
            if let Some(expiration) = session_response.expiration {
                set_session_cookie(cookies, &session_response.session_id, expiration);
            }

            Ok(Json(Response {
                message: session_response.message,
                result: SessionCount {
                    count: session_response.count,
                },
            }))
        }
        Err(e) => Err(Json(ErrorResponse {
            error: format!("Failed to increment session: {e:?}"),
        })),
    }
}

やっていること:

  1. リクエストからデータ抽出(Cookie)
  2. サービス層に処理を委譲
  3. レスポンス形式に変換

やっていないこと:

  • ビジネスロジック
  • データベース操作
  • キャッシュ操作

この「やっていないこと」を守れるかがクリーンアーキテクチャの肝。(守れなくなったらリファクタリングの合図です)

4. サービス層 - ビジネスロジックの集約

セッション管理のコアロジックはここに。RedisとDBの使い分け、有効期限の計算などを担当します。

use crate::repositories::{RepositoryError, SessionRepository};
use chrono::Local;
use std::sync::Arc;
use uuid::Uuid;

pub struct SessionService {
    session_repo: Arc<dyn SessionRepository>,
}

impl SessionService {
    pub fn new(session_repo: Arc<dyn SessionRepository>) -> Self {
        Self { session_repo }
    }

    /// セッションをインクリメントし、必要に応じて新規作成・有効期限設定を行う
    pub async fn increment_session(
        &self,
        session_id: Option<&str>,
    ) -> Result<SessionResponse, RepositoryError> {
        let session_id = match session_id {
            Some(id) if !id.is_empty() => {
                // 既存セッションIDがある場合、有効性をチェック
                let exists = self.session_repo.exists(id).await?;
                if exists {
                    id.to_string()
                } else {
                    // セッションが存在しない場合は新規作成
                    self.create_new_session().await?
                }
            }
            _ => {
                // セッションIDがない場合は新規作成
                self.create_new_session().await?
            }
        };

        // カウントをインクリメント
        let count = self.session_repo.increment(&session_id).await?;

        // 新規セッション(カウント1)の場合は有効期限を設定
        let (message, is_new, expiration) = if count == 1 {
            let expiration = self.calculate_expiration();
            self.session_repo
                .set_expiration(&session_id, expiration.timestamp())
                .await?;
            ("新規セッションが登録されました".to_string(), true, Some(expiration))
        } else {
            ("セッションカウントが更新されました".to_string(), false, None)
        };

        Ok(SessionResponse {
            session_id,
            count,
            message,
            is_new,
            expiration,
        })
    }

    /// セッションの有効期限を計算(翌日の0:00:00)
    fn calculate_expiration(&self) -> chrono::DateTime<Local> {
        use chrono::TimeZone;

        Local::now()
            .date_naive()
            .succ_opt()
            .and_then(|tomorrow| tomorrow.and_hms_opt(0, 0, 0))
            .and_then(|dt| Local.from_local_datetime(&dt).single())
            .unwrap_or_else(|| {
                // フォールバック: 現在時刻から24時間後
                Local::now() + chrono::Duration::hours(24)
            })
    }
}

RedisのINCRコマンドはアトミック。「カウントがズレた」問題とは無縁です。(Excelで集計してた頃が懐かしい...いや、懐かしくない)

設計のポイント:

  • Arc<dyn SessionRepository> でリポジトリを抽象化(モック差し替え可能)
  • 有効期限は「翌日0時」に統一(日次でリセット)
  • エラーハンドリングは Result 型で伝播

5. リポジトリ層 - データアクセスの抽象化

トレイトでインターフェースを定義し、テスト時のモック化を容易にします。

use async_trait::async_trait;
use sea_orm::DbErr;
use chrono::NaiveDate;

#[async_trait]
pub trait SessionCounterRepository: Send + Sync {
    async fn upsert(&self, session_id: &str, count: i64) -> Result<(), DbErr>;
    async fn sum_count_between(
        &self,
        start: NaiveDate,
        end: NaiveDate,
    ) -> Result<i64, DbErr>;
}

実装側:

#[async_trait]
impl SessionCounterRepository for SessionCounterRepositoryImpl {
    async fn sum_count_between(
        &self,
        start: NaiveDate,
        end: NaiveDate,
    ) -> Result<i64, DbErr> {
        let result = SessionCounter::find()
            .filter(session_counter::Column::CreatedAt.gte(start))
            .filter(session_counter::Column::CreatedAt.lte(end))
            .select_only()
            .column_as(session_counter::Column::Count.sum(), "total")
            .into_tuple::<Option<i64>>()
            .one(&self.db)
            .await?;

        Ok(result.flatten().unwrap_or(0))
    }
}

SeaORMのクエリビルダー、結構書きやすいです。型安全なSQLが書ける安心感。

6. 組み込みCronジョブ

tokio::spawnでバックグラウンドタスクを実行します。外部のcronデーモン不要。(設定ファイルが散らばらないの、地味にうれしい)

use super::config::CronJobConfig;
use std::future::Future;
use std::time::Duration;

/// Generic function to spawn a cron job with retry logic
pub fn spawn_cron_job<F, Fut>(config: CronJobConfig, task: F)
where
    F: Fn() -> Fut + Send + 'static,
    Fut: Future<Output = Result<String, Box<dyn std::error::Error + Send + Sync>>> + Send,
{
    tokio::spawn(async move {
        // Validate interval (minimum 1 second)
        let validated_interval = if config.interval_secs < 1 {
            log::warn!(
                "Cron job '{}' interval too short ({}s), using minimum 1s",
                config.name, config.interval_secs
            );
            1
        } else {
            config.interval_secs
        };

        // Handle initial delay if specified
        if let Some(delay) = config.initial_delay {
            log::info!(
                "Starting cron job '{}' with initial delay: {:?}",
                config.name, delay
            );
            tokio::time::sleep(delay).await;
        }

        // Create interval timer
        let mut interval = tokio::time::interval(
            Duration::from_secs(validated_interval)
        );

        loop {
            interval.tick().await;
            log::info!("Running scheduled task: {}", config.name);

            // Retry logic
            let mut retry_count = 0;
            loop {
                match task().await {
                    Ok(message) => {
                        log::info!("{}: {}", config.name, message);
                        break;
                    }
                    Err(e) if retry_count < config.max_retries => {
                        retry_count += 1;
                        log::warn!(
                            "{} failed (attempt {}/{}): {:?}",
                            config.name, retry_count, config.max_retries, e
                        );
                        tokio::time::sleep(
                            Duration::from_secs(config.retry_delay_secs)
                        ).await;
                    }
                    Err(e) => {
                        log::error!(
                            "{} failed after {} attempts: {:?}",
                            config.name, config.max_retries, e
                        );
                        break;
                    }
                }
            }
        }
    });
}

特徴:

  • 設定ベースのジョブ定義
  • 自動リトライ機能(3回失敗したら諦める、という人間味)
  • 構造化ログ出力

60秒ごとにRedis→PostgreSQL同期。これで「Redisが落ちてもデータは守られる」という安心感。(クリック数が消えたら...まあ、非生産的なアプリだからいいんですけど、やっぱり嫌ですよね)

7. Docker Compose環境

# docker-compose.yml
services:
  backend:
    build: ./app/backend
    ports:
      - '8881:8000'
    environment:
      - DATABASE_URL=postgresql://postgres:admin_password@db:5432/kazoerukun
      - REDIS_URL=redis://redis:6379
      - ALLOWED_ORIGINS=http://localhost:3001
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: kazoerukun
      POSTGRES_PASSWORD: admin_password
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - '6380:6379'

volumes:
  postgres-data:

docker-compose up 一発で開発環境が立ち上がる。(環境構築で1日溶かした経験がある人には、この手軽さがわかるはず)

まとめ

Rocket Frameworkを使ったRust APIサーバーの構築パターンを紹介しました。

採用した設計:

  • クリーンアーキテクチャ: Handler → Service → Repository の明確な分離
  • DIコンテナ: 依存性の一元管理とテスタビリティ向上
  • 組み込みCron: tokio::spawnによるバックグラウンドタスク

Rustを選ぶ理由:

  • メモリ安全性(「ランタイムエラーで深夜対応」が減る)
  • 高いパフォーマンス(レイテンシ数msの世界)
  • 型システムによるバグの早期発見(コンパイラが厳しい分、本番が安心)

カゾエルくんは「非生産的の極み」というコンセプトですが、バックエンドは真面目に設計しています。本番運用ではFly.io + Neon + Upstashの構成で月額数ドルの低コスト運用も実現。

「Rustは学習コストが高い」と言われますが、一度身につければ、運用コストの低さで元が取れる。「クリックしたら数が増えるだけ」のアプリでRustを学ぶ...意外とアリかもしれません。

→ 気軽に相談する

この技術が解決した業務課題

記事の技術が実際のプロジェクトでどう活かされているかをご紹介します

【カオナビ 勤怠連携】入退室ログ×カオナビ連携で作業時間96%削減

カオナビと勤怠連携を自動化し、入退室ログをBigQueryへリアルタイム集約。日次集計を2時間→5分に96%削減した導入事例をご紹介します。

事例を読む
About playpark

「あらゆる仕事を楽しむ」をミッションに、業務自動化・AI活用を手がける合同会社です。このブログの運用自体も、自社開発のClaude Code Skillsで完全自動化しています。

会社概要サービス
気軽に相談する
ブログ一覧に戻る

関連記事

すべての記事
【Rust】「非生産的」なWebアプリを作ってみた - Yew + Rocketフルスタック開発
技術Tips
2026年2月23日36分で読める
【Rust】「非生産的」なWebアプリを作ってみた - Yew + Rocketフルスタック開発

フロントエンドもバックエンドも全部Rustで書いたクリックカウンターアプリの技術解説。Yew(WASM)+ Rocket + Redis + PostgreSQLの構成・API設計・Docker Compose環境構築まで、Rust統一フルスタック開発の実践ノウハウをコード付きで紹介します。

RustWebAssemblyAPI開発+2
【OpenClaw Docker セキュリティ】CVE対策で学ぶコンテナハードニング実践 — cap_drop ALL から始める安全運用
技術Tips
2026年3月12日30分で読める
【OpenClaw Docker セキュリティ】CVE対策で学ぶコンテナハードニング実践 — cap_drop ALL から始める安全運用

OpenClawをDocker化しCVE-2026対策を実施。cap_drop ALL・read_only・no-new-privilegesのハードニング設定と、AI開発ツールのコンテナセキュリティ設計を実コード付きで解説します。

OpenClawDockerセキュリティ設定+3
【OpenClaw】Docker構築・モデル切替・Skills開発 実践ガイド — 自分だけのAIアシスタントを作る
技術Tips
2026年3月9日23分で読める
【OpenClaw】Docker構築・モデル切替・Skills開発 実践ガイド — 自分だけのAIアシスタントを作る

OpenClawのDocker本番構築、Gemini/ローカルLLMモデル切替設定、SKILL.md によるカスタムSkills開発まで。Slack導入の次のステップを実践解説します。

OpenClawDockerAI+3

この技術を活用したソリューション

記事の技術を使って、業務課題を解決しませんか?playparkのソリューションをご覧ください。

ソリューション一覧へ