サーバーサイドDart入門

# はじめに
最近Dartを触ることが多く、簡単なAPIくらいならそのまま同じ言語で作った方がコンテキストスイッチしなくていいんじゃね？と思い、調べていたらDartでサーバーサイド開発ができることを知ったので紹介していきます。

# 環境
Dart: v3.5.3

# Shelfについて
ShelfとはDart公式から出ているサーバーアプリケーション用のフレームワークです

https://pub.dev/packages/shelf

これを使えば結構簡単にサーバーサイド開発ができるので今回はShelfを使っていきます。

# 環境構築してみる
dart createにshelfを使ったサーバーアプリケーションを作成するオプションがあるのでそれを使っていきます。

```shell
$ dart create -t server-shelf sample-server-app

Creating sample_server_app using template server-shelf...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  Dockerfile
  .dockerignore
  test/server_test.dart
  bin/server.dart

Running pub get...                     1.2s
  Resolving dependencies...
  Downloading packages...
  Changed 53 dependencies!
  4 packages have newer versions incompatible with dependency constraints.
  Try `dart pub outdated` for more information.

Created project sample_server_app in sample-server-app! In order to get started, run the following commands:

  cd sample-server-app
  dart run bin/server.dart
```

# 動かしてみる
早速動かしてみましょう
アプリケーションを作った時に出てくる`dart run bin/server.dart`で動くみたいなので、一旦それで動かしてみます
```shell
$ dart run bin/server.dart

Server listening on port 8080
```
8080番ポートで動いてるみたいですね


bin/server.dartを見ていきましょう

```dart
import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';

// Configure routes.
final _router = Router()
  ..get('/', _rootHandler)
  ..get('/echo/<message>', _echoHandler);

Response _rootHandler(Request req) {
  return Response.ok('Hello, World!\n');
}

Response _echoHandler(Request request) {
  final message = request.params['message'];
  return Response.ok('$message\n');
}

void main(List<String> args) async {
  // Use any available host or container IP (usually `0.0.0.0`).
  final ip = InternetAddress.anyIPv4;

  // Configure a pipeline that logs requests.
  final handler =
      Pipeline().addMiddleware(logRequests()).addHandler(_router.call);

  // For running in containers, we respect the PORT environment variable.
  final port = int.parse(Platform.environment['PORT'] ?? '8080');
  final server = await serve(handler, ip, port);
  print('Server listening on port ${server.port}');
}
```

難しいことは置いといて、`/`と`/echo/<message>`というエンドポイントがあるっぽい　

試しに叩いてみます
```shell
$ curl http://localhost:8080/

Hello, World!
```

```shell
$ curl http://localhost:8080/echo/I_love_Dart

I_love_Dart
```

無事に動いてますね

# エンドポイントを追加してみる
どうもRouterとやらにどんどんエンドポイントを追加していけば良さそうな予感

ということでこんな感じに書き換えてみる
```dart
final _router = Router()
  ..get('/', _rootHandler)
  ..get('/echo/<message>', _echoHandler)
  ..get('/todos', _todosHandler);

Response _todosHandler(Request req) {
  final todos = {'id': 1, 'title': 'Buy milk'};
  return Response.ok(jsonEncode(todos));
}
```

```shell
$ curl http://localhost:8080/todos

{"id":1,"title":"Buy milk"}
```

`Response.ok`に渡せるのはStringかList<int>、Stream<List<int>>のみのため、jsonEncodeでMap<String, Object>をStringに変換してます。(めっちゃ微妙い)

## エンドポイントをまとめてみる
たくさんエンドポイントが生えてきたらこんな感じになっちゃいますね
```dart
final _router = Router()
  ..get('/', _rootHandler)
  ..get('/echo/<message>', _echoHandler)
  ..get('/todos', _todosHandler)
  ..get('/todo/<id>', _findTodoHandler)
  ..post('/todo', _createTodoHandler)
  ..put('/todo', _updateTodoHandler)
  ..delete('/todo/<id>', _deleteTodoHandler)
  ..
  ..;
```
エンドポイントが増えれば増えるだけ`_router`に対して付け加えなくてはいけないのは結構不便ですよね。

shelfでは以下のようにエンドポイントをまとめることができます。

```dart
final _router = Router()
  ..mount('/todo', _todoRouter.call)
  ..mount('/user', _userRouter.call);

final _todoRouter = Router()
  ..get('/<id>', _findTodoHandler)
  ..post('/', _createTodoHandler);

final _userRouter = Router()
  ..get(...);
```

これでスッキリですね。

# ミドルウェアを追加してみる
リクエストやレスポンスでログを吐かせたい、特定のエンドポイントは認証したいなど実際にリクエストを捌く前にミドルウェアで何かしらしたい場合はよくあると思います。

shelfでは以下のようにミドルウェアを追加できます。

```dart
final authMiddleware = createMiddleware(
  requestHandler: (req) {
    // 何かしら認証処理
  },
);

void main(List<String> args) async {
  // Use any available host or container IP (usually `0.0.0.0`).
  final ip = InternetAddress.anyIPv4;

  // Configure a pipeline that logs requests.
  final handler = Pipeline()
      .addMiddleware(logRequests())
      .addMiddleware(authMiddleware)
      .addHandler(_router.call);

  // For running in containers, we respect the PORT environment variable.
  final port = int.parse(Platform.environment['PORT'] ?? '8080');
  final server = await serve(handler, ip, port);
  print('Server listening on port ${server.port}');
}
```

`createMiddleware`関数が提供されているので簡単にミドルウェアが作れます。

# ホットリロードしてみる
コードを書き換えたらホットリロードしたいですよね。僕はしたいです。
shelf自体にホットリロードの機能は搭載されていないので、今回はパッケージを使います。

https://pub.dev/packages/shelf_hotreload

```dart
void main(List<String> args) async {
  withHotreload(() {
    // Use any available host or container IP (usually `0.0.0.0`).
    final ip = InternetAddress.anyIPv4;

    // Configure a pipeline that logs requests.
    final handler = Pipeline()
        .addMiddleware(logRequests())
        .addMiddleware(authMiddleware)
        .addHandler(_router.call);

    // For running in containers, we respect the PORT environment variable.
    final port = int.parse(Platform.environment['PORT'] ?? '8080');
    return await serve(handler, ip, port);
  }
}
```

# 感想
Dartは易しい言語だし、functionalに書けるのは非常にいいし、ある程度機能も揃ってるしでFlutterアプリエンジニアがシュッとAPI書くのにはちょうどいいのかなと思いました。

実行ファイル作ってくれるるDockerfileもバンドルされてるので、デプロイもある程度簡単そうな印象です。

# 感想(本音)
Dartで書きたいならshelf使えばいいけど、別に言語にこだわらないならExpressでよくない？というのが素直な感想でした。