26
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2022-09-20

Nitroって何?

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

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

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

筆者プロフィール

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

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

Nitroの良いところ

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

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

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

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

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

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

Nitro を軽く動かしてみる

環境構築

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

mkdir nitro
cd nitro
yarn add -D nitropack

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

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

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

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 で動かしてみようか。

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

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

Nitroのルーティング

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

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

という形式で公開されるんだ。これは、Nitroと同じく unjs プロジェクトのツールの一つである 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プロジェクト内に、上記を満たす機能を実装していくからね。

まずは認可機能から。

要件1 「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 の各種関数などを、import文なしで使えるようになってるよ。

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

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

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

Nitro では、各種ルート上のファイルに xxxx.get.tsxxxx.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/h3useBody にジェネリクスで型を補完した上で、リクエストボディを取得するようにしているよ。

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 コマンドを実行して、動きをみてみよう。

# 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

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

~ % # 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 すればプロダクションビルドをできるようになってるよ。

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

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サーバで動く用の資材 が作成されているから、次のようにして起動できるよ。

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

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

実例を一つ見せると、例えば 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 の3つの特別なディレクトリがルーティング対象だよ
  • 上記のうち middleware はサーバーミドルウェアとしての役割を果たしてくれていて、サーバへの通信ごとに処理を噛ませることができるよ。認可の実装にはもってこいの機能だね

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

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

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

26
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?