mcpField
S
serverpod-webserver

serverpod-webserver

github.com/serverpod開発3.3k44 回インストール1 日前に更新
リポジトリ

serverpod-webserver について

Serverpod のオプションである Relic ベースの Web サーバーでルートを構築するスキルです。共有セッションを使った HTTP リクエスト処理を、ルーティング、ミドルウェア、Web コンテンツの面から案内します。

主な機能

  1. 01HTTP メソッドを制限したルート定義
  2. 02型付きパス・クエリパラメーターの読み取り
  3. 03文字列やデータストリームからのレスポンス作成
  4. 04フォールバックルートとミドルウェアの設定
  5. 05静的ファイルと Web アプリの配信

活用シーン

  1. 01Serverpod API と並行して HTTP ルートを追加
  2. 02API キーミドルウェアによるリクエスト検証
  3. 03ホストまたはパスに応じたリクエストのルーティング
  4. 04静的ファイルや Web アプリコンテンツの配信

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/serverpod/serverpod --skill serverpod-webserver

インストール後、Claude Code で/serverpod-webserverを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
server-rendered-html.md
spa-and-flutter-web.md
static-files.md

詳細

Serverpod Web サーバー(Relic)

Relic 上に構築されており、メインサーバーと同じ Session(DB、ログ、認証)を共有します。このスキルは、メイン API サーバー(デフォルトポート 8080)ではなく、オプションの webServer リスナー(デフォルトポート 8082)を対象としています。

ルート

Route を拡張し、handleCall(Session, Request) を実装します。pod.start() の前に登録してください:

class HelloRoute extends Route {
  @override
  Future<Result> handleCall(Session session, Request request) async {
    return Response.ok(
      body: Body.fromString(
        jsonEncode({'message': 'Hello'}),
        mimeType: MimeType.json,
      ),
    );
  }
}

pod.webServer.addRoute(HelloRoute(), '/api/hello');

ルートは登録順に照合されます。Session はエンドポイントと同じように、DB、ログ、認証へのアクセスを提供します。

HTTP メソッド

ルートが受け付けるメソッドを制限し(デフォルトは GET のみ)、request.method で分岐します:

class UserRoute extends Route {
  UserRoute() : super(methods: {Method.get, Method.post, Method.delete});

  @override
  Future<Result> handleCall(Session session, Request request) async {
    if (request.method == Method.post) {
      final data = jsonDecode(await request.readAsString());
      // ...
      return Response(201);
    }
    // ...
  }
}

パスパラメーター

pod.webServer.addRoute(UserRoute(), '/api/users/:id');
pod.webServer.addRoute(route, '/:userId/posts/:postId');

型付きパラメーターにアクセスします:

class UserRoute extends Route {
  static const _idParam = IntPathParam(#id);

  @override
  Future<Result> handleCall(Session session, Request request) async {
    int userId = request.pathParameters.get(_idParam);
    // ...
  }
}

生の値には request.pathParameters.raw[#id] でアクセスします。型付きクエリパラメーターも同様に、IntQueryParam('page') と request.queryParameters.get(...) を使います。生の値には request.queryParameters.raw['query'] でアクセスします。

ワイルドカード

pod.webServer.addRoute(route, '/item/*');   // One segment: /item/foo
pod.webServer.addRoute(route, '/item/**');  // Tail-match: /item/foo/bar/baz

** はパスの末尾でのみ使用できます。一致したパスには request.remainingPath でアクセスします。

ヘッダーと本文

final userAgent = request.headers.userAgent;
final contentLength = request.headers.contentLength;
final auth = request.headers.authorization;
final apiKey = request.headers['X-API-Key']?.first;

final body = await request.readAsString();  // JSON, form data
final stream = request.read();              // Stream for large uploads

本文を読み取れるのは一度だけです。

レスポンスの種類

Response には、一般的なステータス用の名前付きコンストラクターがあります:ok、noContent、notModified、movedPermanently、found、seeOther、badRequest、unauthorized、forbidden、notFound、notImplemented、internalServerError。それ以外のステータスには、名前なしコンストラクターを使用します(例:Response(201))。本文は Body.fromString(content, mimeType: MimeType.json)、Body.fromData(...)、または Body.fromDataStream(...) で構築します。

フォールバックルート

pod.webServer.fallbackRoute = NotFoundRoute();

他のルートに一致しないリクエストを処理します。

ルートモジュール(injectIn)

injectIn() をオーバーライドして、関連するエンドポイントをまとめます:

class UserCrudModule extends Route {
  @override
  void injectIn(RelicRouter router) {
    router
      ..get('/', _list)
      ..get('/:id', _get);
  }

  static const _idParam = IntPathParam(#id);

  Future<Result> _list(Request request) async {
    final session = await request.session;
    final users = await User.db.find(session);
    return Response.ok(
      body: Body.fromString(jsonEncode(users.map((u) => u.toJson()).toList()),
        mimeType: MimeType.json),
    );
  }

  Future<Result> _get(Request request) async {
    final session = await request.session;
    final user = await User.db.findById(session, request.pathParameters.get(_idParam));
    if (user == null) return Response.notFound();
    return Response.ok(
      body: Body.fromString(jsonEncode(user.toJson()), mimeType: MimeType.json),
    );
  }
}

pod.webServer.addRoute(UserCrudModule(), '/api/users');
// Creates GET /api/users and GET /api/users/:id

注:injectIn ハンドラーが受け取るのは Request のみです。Session には await request.session でアクセスしてください。

ミドルウェア

ミドルウェアはハンドラーをラップします。パスプレフィックスを指定して登録します:

Handler apiKeyMiddleware(Handler next) {
  return (Request request) async {
    final apiKey = request.headers['X-API-Key']?.firstOrNull;
    if (apiKey == null || !await isValidApiKey(apiKey)) {
      return Response.unauthorized(body: Body.fromString('API key required'));
    }
    return await next(request);
  };
}

pod.webServer.addMiddleware(apiKeyMiddleware, '/api');
実行順序

より具体的なパスほど内側のミドルウェアとして実行されます。同じパス内では、登録順に実行されます:

pod.webServer.addMiddleware(rateLimitMiddleware, '/api/users'); // Inner (last before handler)
pod.webServer.addMiddleware(apiKeyMiddleware, '/api');           // Outer (first)

/api/users/list の場合:apiKeyMiddleware → rateLimitMiddleware → ハンドラー → rateLimitMiddleware → apiKeyMiddleware。

リクエストスコープのデータ(ContextProperty)

リクエストを変更せずに、ミドルウェアからルートへデータを渡します:

final _tenantProperty = ContextProperty<String>('tenant');

extension TenantEx on Request {
  String get tenant => _tenantProperty.get(this);
}

Handler tenantMiddleware(Handler next) {
  return (Request request) async {
    final session = await request.session;
    final tenant = await extractTenant(session, request.headers.host);
    if (tenant == null) return Response.notFound();
    _tenantProperty[request] = tenant;
    return await next(request);
  };
}

// In route:
final tenant = request.tenant;

リクエストの完了時にデータは自動的にクリーンアップされます。ホスト固有のミドルウェア:pod.webServer.addMiddleware(mw, '/api', host: 'api.example.com')。

仮想ホストルーティング

特定の Host ヘッダーにルートやミドルウェアを限定します:

pod.webServer.addRoute(ApiRoute(), '/v1');  // ApiRoute has host: 'api.example.com'
pod.webServer.addRoute(SpaRoute(webDir, fallback: index, host: 'www.example.com'), '/');
pod.webServer.addRoute(HealthRoute(), '/health');  // All hosts (default)

すべてのルートタイプが host に対応しています:Route、StaticRoute、SpaRoute、FlutterRoute。

ファイルと Web アプリの配信

これらについては、このスキルディレクトリ内のリファレンスファイルで説明しています。タスクに合ったファイルを読んでください:

コメント