Nitro + TypeScript で作る軽量APIサーバ

## Nitroって何？

**`Universal JavaScript Servers`** を自称するTypeScript製の軽量サーバだよ。
`Vue` や `Nuxt` の開発メンバーを中心に作られていて、`Nuxt3` にも組み込まれているよ。

GitHubのリポジトリはこちら。

https://github.com/unjs/nitro

この [unjs](https://github.com/unjs) というプロジェクトには `Unified JavaScript Tools` という意味合いで、さまざまなTypeScript製ツールが作られていて、他にも [unjs/ohmyfetch](https://github.com/unjs/ohmyfetch) など便利なツールが開発されているよ。unjsの各種ツールは、Nuxt3のエコシステムとしても使われているから、これからも継続的なサポートと機能向上が期待できるよ。

## 筆者プロフィール

[Kenpal株式会社](https://www.kenpalinc.com/) でITエンジニアとして色々いじってる faable01 です。かつては創作仲間と小説を書いたり、製菓業界で楽しくやっていたはずが、紆余曲折を経て、サーバーレス技術を触るのが好きなITエンジニアになっていました。AWSのIaC兼サーバレス爆速開発ツール **「SST」** が好きです。個人ブログでもたまに記事を書いています。

https://signote.link

それから、業務日報SaaS **「RevisNote」** を運営しています。リッチテキストでの日報と、短文SNS感のある分報を書けるのが特徴で、組織に所属する人数での従量課金制です。アカウント開設後すぐ使えて、無料プランもあるから、気軽にお試しください。

https://revisnote.com?1710501811980


## Nitroの良いところ

全てを `devDependencies` にするよ。具体的には、ビルド後の資材が必要最低限のもので `.output` ディレクトリにまとめられるから、 **「巨大な `node_modules` を AWS Lambda にあげて、コールドスタート時のあまりの遅さに悩まされる」** なんて事態を回避できるよ。

ここら辺の話は、Nitro を組み込んだ Nuxt3 においても同様のメリットを享受できてるからね。

https://nuxtjs.org/announcements/nuxt3-rc/#why-nitro

また、**`Universal JavaScript Servers`** を自称するだけあって、実際に色んな環境に、簡単にビルド＆デプロイできるよ。

具体的には、Nitro は次の環境へのビルド＆デプロイをサポートしているよ。

- 通常のNode.jsサーバ
- AWS Lambda
- Azure
- Cloudflare
- DigitalOcean
- Firebase
- Heroku
- Layer0
- Netlify
- Render.com
- StormKit
- Vercel

https://nitro.unjs.io/deploy

前述の **「全てを `devDependencies` にする」** という強みと組み合わさって、どんな環境でもストレスなくビルド＆デプロイできるというのが Nitro の魅力だね。

## Nitro を軽く動かしてみる

### 環境構築

`nitro` というディレクトリを作って、その中に環境構築していくよ。

```zsh
mkdir nitro
cd nitro
yarn add -D nitropack
```

nitroディレクトリ直下に `tsconfig.json` を置いて...

```json:tsconfig.json
{
  "extends": "./.nitro/types/tsconfig.json",
  "compilerOptions": {
    "strict": true,
  }
}
```

同じく nitroディレクトリ直下の `package.json` に必要なスクリプトを追加してやって...

```diff:package.json
 {
+   "scripts": {
+     "dev": "nitropack dev",
+     "build": "nitropack build"
+   },
   "devDependencies": {
     "nitropack": "^0.5.3"
   }
 }
```

これだけで環境構築は完了！ 軽く動作確認してみよっか。

### 動作確認

まずは次のファイルを作成しよう。

```routes/hello/[name].ts
export default eventHandler(event =>
  'Hello ' + event.context.params.name);
```

このとき型に関するエラーがエディタ上で出るかもしれないけど、これは `yarn dev` で `.nitro` ディレクトリが作成されたら直るよ。

そういうわけで `routes/hello/[name].ts` を実装できたら、早速 `yarn dev` で動かしてみようか。

```zsh
% curl http://localhost:3000/hello/world
Hello world
```

無事に応答 `Hello world` が返ってきたね。

### Nitroのルーティング

見ての通り、ここで作成した `routes/hello/[name].ts` は

- `http://localhost:3000/hello/任意の文字列`

という形式で公開されるんだ。これは、Nitroと同じく `unjs` プロジェクトのツールの一つである [unjs/h3](https://github.com/unjs/h3) によってルーティングされているよ。

自動でルーティングされるのは `routes` ディレクトリだけでなくて、他にもこんなディレクトリが存在するよ。

- **routesディレクトリ :** 
routes以下が、そのままのパス名で応答可能なURLとして公開される。

- **apiディレクトリ :** 
api以下が、`api/パス名` で応答可能なURLとして公開される。

- **middlewareディレクトリ :**
ミドルウェアとして、サーバへのアクセスが発生する度に実行するロジックを置くことができる。認可の実装などで役立つ。
**何らかの値をreturnしなければ後続処理は、URLに対応する `routes` もしくは `api` に繋がる。**
逆に認可エラーなどを理由に、ここでアクセスを弾きたければ、middlewareで応答を返してやることができる。

次は、ちょっとした REST-API を作ってみるよ。

## Nitro で REST-API を作ってみる

こんな要件を満たす REST-API を Nitro で簡略的に作ってみるよ。

- 要件1. GET通信以外は、トークンによる認可を要する
- 要件2. ユーザ情報を保持し、GETで全件取得できる
- 要件3. POSTでリクエストボディに渡した名前のユーザを生成できる
- 要件4. DELETEで全件削除できる

ここまでに作成したNitroプロジェクト内に、上記を満たす機能を実装していくからね。

まずは認可機能から。

### 要件１ 「GET通信以外は、トークンによる認可を要する」 の実装

サーバーミドルウェアとしての役割を果たしてくれる `middleware` ディレクトリに、認可機能を実装するよ。

こんな感じだね。

```middleware/auth.ts
/**
 * Nitro の Auto Import 機能により、
 * unjs/h3 の useBody, useMethod, getHeaderなど
 * 基本的な組み込み関数を import せずに使える。
 * 
 * なお unjs/h3 には、下記のリンクにあるような便利メソッドが多々用意されている。
 * 
 * https://github.com/unjs/h3
 */
export default eventHandler(event => {
  // GETなら検証しない
  const method = useMethod(event);
  if (method === "GET") return;

  // リクエストヘッダの小文字・大文字問題を、unjs/h3 が解決してくれる（getHeader）
  const _authorization = getHeader(event, "Authorization");
  const authorization = Array.isArray(_authorization)
    ? _authorization[0] : _authorization;
  
  // リクエストヘッダからトークンを取得
  const token = authorization?.replace('Bearer ', '');
  if (!verifyToken(token)) {
    // トークンが無効な場合はエラーを返
    event.res.statusCode = 401;
    return 'Unauthorized';
  }
});

/** トークンを検証する関数 */
const verifyToken = (token: string | undefined) => {
  // ---- 実装の内容は割愛 ----
  return !!token;
};
```

コメントにも書いたように、Nitro　には Auto Import 機能があって、 [unjs/h3](https://github.com/unjs/h3) の各種関数などを、import文なしで使えるようになってるよ。

これは具体的には、開発環境上では `yarn dev` した時に生成される `.nitro` ディレクトリ内でグローバルインポートしてくれてるんだね。

（こういうのは人によって好みが異なるから、もし自動インポートが嫌いな人は、個別で `unjs/h3` パッケージなどから必要な機能のimport文を書いていってね）

### 要件2 「ユーザ情報を保持し、GETで全件取得できる」 の実装

Nitro では、各種ルート上のファイルに `xxxx.get.ts` や `xxxx.post.ts` などの名前をつけてやることで、ルーティングを特定のHTTPメソッドに限定することができるよ。

例えば `api/hoge.get.ts` なら `http://localhost:3000/api/hoge` にGET通信時でアクセスした時にしかコールされないし、`api/hoge.post.ts` なら `http://localhost:3000/api/hoge` にPOST通信時でアクセスした時にしかコールされないというわけだね。

そういうわけだから、ユーザ情報を全件取得するGET通信時のルートは、次のように実装するよ。


```api/users.get.ts
import { usersData } from "../data/usersData";

/** 全てのユーザを取得する */
export default eventHandler(() => ({
  users: usersData,
}));
```

ちなみに、アプリケーションが保持しているユーザ情報は、簡略的に次のように実装しているからね。

```data/usersData.ts
export const usersData = [] as string[];
```

### 要件3 「POSTでリクエストボディに渡した名前のユーザを生成できる」 の実装

今度はユーザ情報をPOSTで投稿するAPIなので、ファイル名は `api/users.post.ts` にするよ。

ここでも Nitro の Auto Import 機能で便利メソッドを引っ張ってきていて、`unjs/h3` の `useBody`　にジェネリクスで型を補完した上で、リクエストボディを取得するようにしているよ。

```api/users.post.ts
import { usersData } from "../data/usersData";

type Body = Partial<{user: string}>;

/** リクエストボディで渡されたユーザを追加する */
export default eventHandler(async event => {
  const body = await useBody<Body>(event);
  if (!body.user) {
    throw new Error("Missing user");
  }
  usersData.push(body.user);
  return {
    users: usersData,
  };
});
```

そういえばここまで当たり前のように、APIのコードが応答でJavaScriptのオブジェクトを返しているけれど、Nitro では「何を応答しているか」を解析して、自動的に適切な `content-type` をレスポンスヘッダに設定してくれているからね。

今回はオブジェクトを返しているから、Nitroは自動的に、 `content-type: application/json` をレスポンスに設定してくれているよ。

### 要件4 「DELETEで全件削除できる」 の実装

ここでもやっぱり、ファイル名でHTTPメソッドをDELETEに限定するよ。
こんなふうに実装すればいいね。

```api/users.delete.ts
import { usersData } from "../data/usersData";

/** 全てのユーザを削除する */
export default eventHandler(() => {
  usersData.splice(0, usersData.length);
  return {
    users: usersData,
  };
});
```

### 実装した REST-API を動かしてみる

認可機能を実装したサーバーミドルウェアがちゃんとアクセスを弾いてくれるかな？　というところに着目して試してみるよ。

次の curl コマンドを実行して、動きをみてみよう。

```zsh
# GET
curl http://localhost:3000/api/users

# POST（トークンなし）
curl -X POST -H "Content-Type: application/json" \
  -d '{"user":"hoge"}' \
http://localhost:3000/api/users

# POST（トークンあり）
curl -X POST -H "Content-Type: application/json" \
  -H 'Authorization: Bearer XXXX' \
  -d '{"user":"hoge"}' \
  http://localhost:3000/api/users

# DELETE（トークンなし）
curl -X DELETE \
  http://localhost:3000/api/users

# DELETE（トークンあり）
curl -X DELETE -H 'Authorization: Bearer XXXX' \
  http://localhost:3000/api/users
```

一つずつ実行してみると、結果はこんなふうになるよ。

```zsh
~ % # GET
~ % curl http://localhost:3000/api/users
{
  "users": []
}


~ % # POST（トークンなし）
~ % curl -X POST -H "Content-Type: application/json" \
  -d '{"user":"hoge"}' \
http://localhost:3000/api/users
Unauthorized


~ % # POST（トークンあり）
~ % curl -X POST -H "Content-Type: application/json" \
  -H 'Authorization: Bearer XXXX' \
  -d '{"user":"hoge"}' \
  http://localhost:3000/api/users
{
  "users": [
    "hoge"
  ]
}


~ % # DELETE（トークンなし）
~ % curl -X DELETE \
  http://localhost:3000/api/users
Unauthorized


~ % # DELETE（トークンあり）
~ % curl -X DELETE -H 'Authorization: Bearer XXXX' \
  http://localhost:3000/api/users
{
  "users": []
}
```

無事に、GET通信以外ではトークンなしの場合アクセスを弾いてくれてるね。

これで、認可機能付きのごく簡単なREST-APIは完成だよ。

## おまけ: productionビルド

環境構築の時に package.json の scripts に `"build": "nitropack build"` を追加しているから、`yarn build` すればプロダクションビルドをできるようになってるよ。

実際にやってみると...

```zsh
nitro % yarn build
yarn run v1.22.18
warning package.json: No license field
$ nitropack build
✔ Generated public .output/public                                                                                         nitro 12:04:59
ℹ Building Nitro Server (preset: node-server)                                                                             nitro 12:04:59
✔ Nitro server built                                                                                                      nitro 12:05:00
  ├─ .output/server/package.json (297 B) (179 B gzip)
  ├─ .output/server/index.mjs (445 B) (254 B gzip)
  ├─ .output/server/chunks/usersData.mjs.map (172 B) (153 B gzip)
  ├─ .output/server/chunks/usersData.mjs (89 B) (93 B gzip)
  ├─ .output/server/chunks/users.post.mjs.map (472 B) (228 B gzip)
  ├─ .output/server/chunks/users.post.mjs (705 B) (360 B gzip)
  ├─ .output/server/chunks/users.get.mjs.map (240 B) (178 B gzip)
  ├─ .output/server/chunks/users.get.mjs (534 B) (278 B gzip)
  ├─ .output/server/chunks/users.delete.mjs.map (336 B) (199 B gzip)
  ├─ .output/server/chunks/users.delete.mjs (600 B) (304 B gzip)
  ├─ .output/server/chunks/ping.mjs.map (201 B) (154 B gzip)
  ├─ .output/server/chunks/ping.mjs (451 B) (251 B gzip)
  ├─ .output/server/chunks/nitro/node-server.mjs.map (138 kB) (9.59 kB gzip)
  ├─ .output/server/chunks/nitro/node-server.mjs (34 kB) (9.49 kB gzip)
  ├─ .output/server/chunks/_name_.mjs.map (266 B) (181 B gzip)
  └─ .output/server/chunks/_name_.mjs (492 B) (273 B gzip)
Σ Total size: 698 kB (149 kB gzip)
✔ You can preview this build using node .output/server/index.mjs                                                          nitro 12:05:00
✨  Done in 1.78s.
```

こんな感じで、 `.output` ディレクトリに、ビルド済みの資材が生成されるよ。

生成された資材は、ここでは Node.jsサーバで動く用の資材 が作成されているから、次のようにして起動できるよ。

```zsh
nitro % node .output/server/index.mjs     
Listening http://[::]:3000
```

ここではビルド設定を特に指定していないから、 **Node.jsサーバで動く用の資材** が作成されたけど、もし AWS Lambda や Firebase など、そのほか環境にデプロイしたい人は、以下のリンクを見ながら設定を追加してやればできるよ。

https://nitro.unjs.io/deploy

実例を一つ見せると、例えば AWS Lambda 向けにビルドするなら、環境変数を使ってこんな風にビルドできるよ。

```
nitro % NITRO_PRESET=aws-lambda yarn build
yarn run v1.22.18
warning package.json: No license field
$ nitropack build
✔ Generated public .output/public                                                                                         nitro 12:08:43
ℹ Building Nitro Server (preset: aws-lambda)                                                                              nitro 12:08:43
✔ Nitro server built                                                                                                      nitro 12:08:43
  ├─ .output/server/package.json (284 B) (173 B gzip)
  ├─ .output/server/index.mjs (370 B) (240 B gzip)
  ├─ .output/server/chunks/usersData.mjs.map (172 B) (153 B gzip)
  ├─ .output/server/chunks/usersData.mjs (89 B) (93 B gzip)
  ├─ .output/server/chunks/users.post.mjs.map (467 B) (228 B gzip)
  ├─ .output/server/chunks/users.post.mjs (630 B) (344 B gzip)
  ├─ .output/server/chunks/users.get.mjs.map (235 B) (177 B gzip)
  ├─ .output/server/chunks/users.get.mjs (459 B) (263 B gzip)
  ├─ .output/server/chunks/users.delete.mjs.map (331 B) (199 B gzip)
  ├─ .output/server/chunks/users.delete.mjs (525 B) (290 B gzip)
  ├─ .output/server/chunks/ping.mjs.map (196 B) (154 B gzip)
  ├─ .output/server/chunks/ping.mjs (376 B) (236 B gzip)
  ├─ .output/server/chunks/nitro/aws-lambda.mjs.map (40.6 kB) (6.38 kB gzip)
  ├─ .output/server/chunks/nitro/aws-lambda.mjs (31.5 kB) (8.78 kB gzip)
  ├─ .output/server/chunks/_name_.mjs.map (261 B) (180 B gzip)
  └─ .output/server/chunks/_name_.mjs (417 B) (257 B gzip)
Σ Total size: 591 kB (142 kB gzip)
✨  Done in 1.20s.
```

これだけで、AWS Lambda 用の資材をビルドできたよ。
実際に Lambda 用の資材かをお手軽に試すために、ビルドした資材のエントリーポイントを、さっきのNode.js環境用資材と同じように node コマンドで実行してみよう。

```
nitro % node .output/server/index.mjs
nitro % 
```

Lambda用ビルドは、Node.jsサーバのような動きをするものじゃないから、こんなふうに node コマンドで実行しても localhost:3000 でサーバが立ち上がったりはしないのが正しいね。

実際に、この資材をLambdaにデプロイしてやれば、ちゃんと動くようになるはずだよ。工夫すれば、 `AWS Lambda@Edge` 向けのデプロイ資材も、ここからすぐに作っていけるよ（ちょっと手間だけどね）。

そういうわけで、Nitroならこんなふうに、環境変数一つでデプロイ先の環境に合わせたビルドをできるよ。公式ドキュメントにも書いてあるけど、環境変数ではなく設定ファイルからビルド設定を行うこともできるからね。

## まとめ

* Nitro は Nuxt3 にも組み込まれている　**ユニバーサルJavaScriptサーバー** だよ
* 全てを `devDependencies` にしてくれる性質があって、ビルド後の資材は `.output` ディレクトリに必要なものだけでまとめられるよ
* AWS Lambda や Firebase、Vercel など、さまざまな環境向けの資材を環境変数ひとつ（あるいは設定ファイル）で簡単にビルドできるよ
* サーバを実装するときは、`routes`, `api`, `middleware` の３つの特別なディレクトリがルーティング対象だよ
* 上記のうち `middleware` はサーバーミドルウェアとしての役割を果たしてくれていて、サーバへの通信ごとに処理を噛ませることができるよ。認可の実装にはもってこいの機能だね

それから、記事の中では紹介できなかったけど、ビルドした資材を minify したければ、設定ファイルで明示してやることで可能になるよ。

https://nitro.unjs.io/config#minify

Nuxt3 のサーバーサイドの中核となっている Nitro は、Nitro単品でも APIサーバの構築など色々と便利に使えるよ。よかったら、使ってみてね。

あと、Nitro と Nuxt3 は密接に繋がってるから、使い勝手は（特にサーバーサイドは）ほぼ同じだよ。Nuxt3も便利で書き心地良いから、きっと楽しめると思うよ。