FlutterKaigi mini #4 @Kyoto: dart_frogではじめるバックエンド開発

こちらは、2025/04/26、京都で行われたFlutterKaigi mini #4 @Kyoto の資料になってます。

使おうと思った理由

AI Agent Hackathon with Google Cloudに参加
条件:AIはGEMIN使え & Googleのクラウドサービスを使え

  • GeminiAPIのAPIキーをバックエンドに置こう
  • Functionに挑むも、Java、TypeScript、Python依存関係で落ちるorz
    →そうだ、Dartでサーバを作ろう(とりあえず、コストは考えない)

DartでWebサーバ

  • shelf: 軽量。面倒くさそう(サーバ起動がmain文に入ってるw)Publisher: tetools.dart.dev
  • serverpod: 重量(DB, ORMを含む)
  • frog: 軽量。わかりやすそう。Publisher: verygood.ventures

作業

  • インストール
    dart pub global activate dart_frog_cli
    /Users/user_name/.pub-cache/bin/dart_frog にできた

  • プロジェクト作成と移動
    dart_frog create frog_project_name
    cd frog_project_name

  • プロジェクト起動
    dart_frog dev
    確認→ http://localhost:8080/

ディレクトリの中身

README.md
analysis_options.yaml
pubspec.yaml
test
routes/index.dart ← NEW!

index.dart

パス:http:localhost:8080

import 'package:dart_frog/dart_frog.dart';

Response onRequest(RequestContext context) {
  return Response(body: 'Welcome to Dart Frog!');
}

個人的におすすめのディレクトリ構成

libを作って、通常のDartクラスのモデルやロジックを作成する
routesの中は、パス処理だけして、細かい処理はlib内のクラスで実施する

ー チャットのデータクラス

mkdir lib

vi lib/chat.dart

class Chat {
  const Chat(this.id, this.message);
  final int id;
  final String message;

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'message': message,
    };
  }
}
  • データベースのクラス

mkdir lib

vi lib/database.dart

import 'dart:async';

import 'package:frog_project_name/chat.dart';

class DataBase {
  final _chatList = [
    Chat(1, 'one'),
    Chat(2, 'two'),
    Chat(3, 'three'),
    Chat(4, 'four'),
  ];

  Future<Chat?> selectChatById(int id) async =>
      _chatList.where((e) => e.id == id).firstOrNull;

  Future<List<Chat>> selectAllChat() async => _chatList;
}
  • ミドルウェア(システム内の共通クラスを取り扱う)

vi routes/_middleware.dart

import 'package:dart_frog/dart_frog.dart';
import 'package:frog_project_name/database.dart';

final _dataBase = DataBase();

final _databaseProvider = provider<DataBase>((context) => _dataBase);

Handler middleware(Handler handler) {
  return handler.use(_databaseProvider);
}

mkdir routes/chat

vi routes/chat/index.dart

import 'dart:convert';
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:frog_project_name/database.dart';

/// curl http://localhost:8080/chat/
Future<Response> onRequest(RequestContext context) async {
  return switch (context.request.method) {
    HttpMethod.get => _onGet(context),
    _ => Future.value(Response(statusCode: HttpStatus.methodNotAllowed)),
  };
}

Future<Response> _onGet(RequestContext context) async {
  final database = context.read<DataBase>();

  final list = await database.selectAllChat();
  return Response.json(body: jsonEncode(list));
}

vi routes/chat/\[id\].dart

import 'dart:convert';
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:frog_project_name/database.dart';

/// curl http://localhost:8080/chat/1
Future<Response> onRequest(RequestContext context, String id) async {
  final intId = int.parse(id);
  return switch (context.request.method) {
    HttpMethod.get => _onGet(context, intId),
    _ => Future.value(Response(statusCode: HttpStatus.methodNotAllowed)),
  };
}

Future<Response> _onGet(RequestContext context, int chatId) async {
  final database = context.read<DataBase>();

  final chat = await database.selectChatById(chatId);
  if (chat == null) {
    return Response(statusCode: HttpStatus.notFound);
  }

  return Response.json(body: jsonEncode(chat));
}
final modelName = context.request.uri.queryParameters['model'];

デプロイ

  • デプロイ用のWebサーバ用の一式の作成
    dart_frog build

  • gloudへのデプロイ
    gcloud run deploy [SERVICE_NAME] –source build –project=[PROJECT_ID] –region=[REGION] –allow-unauthenticated
    (例) gcloud run deploy geminiapi –source build –project=gen-lang-client-080 –region=asia-northeast2 –allow-unauthenticated

サーバの環境変数:Platform.environment をDartソースで使う (String.fromEnvironmentではない)

final key1 = Platform.environment['KEY1'];

感想

gcloudの設定が一番難しかった。インフラ、やりたくねー

routes内のクラスはブレークポイントを使ったデバッグができん(知ってたら教えて)