ゼロから Node.js + Express + MongoDB で爆速で REST API を作る

# 概要

Node.js + Express + MongoDB で REST API を作ってみます。
細かい説明は各リンク先を読んでもらうとして、**とにかく動くものを爆速で作る**というのが今回の目的です。

これらを組み合わせれば、例えば `/users/:id` というAPIでユーザ情報をDB取得からするというコードがこんなに簡単に書けます。

```typescript
app.get("/users/:id", async (req, res) => {
    const id = req.params.id
    const users = await usersResource.find({ _id: id });
    res.status(200).json(users);
});
```

是非、一緒に手を動かしてやってみましょう。

なお、サンプルコードはこちらに置いてあります。

https://github.com/Nyanchu/sample-express-mongoose

# 前提

### 環境

* MacOS
* Homebrew
  * 未インストールの方は [公式サイト](https://brew.sh/index_ja) を参照
* Docker
  * 未インストールの方は [公式ドキュメント](https://docs.docker.jp/desktop/toc.html) を参照

### 本記事を読むにあたって

以下の知識があることを前提に進めていきます。

* JavaScript, TypeScript, Node.js のある程度の知識
* Dockerのある程度の知識(なくてもなんとなくでいけます)

# 今回使うもの

今回使うものを説明しつつ、爆速でインストールも済ませていきます。

### Node.js

まずは Node.js のインストールをします。既にインストール済みの方は飛ばしてください。
自分は Node.js のバージョンを切り替えたくなることが多いので [nvm ](https://github.com/nvm-sh/nvm) を使ってインストールしていますが、ここでは速度重視のため直に入れることにします。

```bash
$ brew install node
```

コマンド実行後「PATHを通してね」という案内が出るのでその通りにパスを通しましょう。自分は zsh を使っているので `~/.zshrc` に以下を追記しました。追記後に `source ~/.zshrc` をお忘れなく。

```sh
export PATH=/usr/local/opt/node/bin:$PATH
```

ではインストールできたか確認してみます。

```bash
$ node -v
v18.12.1
```

OKです。

### TypeScript

まずは Node.js のプロジェクトを作ります

```bash
# プロジェクトディレクトリ作成
$ mkdir sample-express-mongoose
$ cd sample-express-mongoose
# package.json の作成
$ npm init --yes
```

出来上がった package.json の中身に1行だけ追加する必要があります。

```diff
   "version": "1.0.0",
   "description": "Express + Mongoose の REST API 作成サンプルです。",
   "main": "index.js",
+  "type": "module",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
```

続いて TypeScript をインストール・設定していきます。
ここでついでに便利ツール ts-node も入れています(あとで使います)。

```bash
# TypeScript をインストール
$ npm install --save-dev typescript @types/node
$ npm install --save-dev ts-node
# tsconfig.json 作成
$ npx tsc --init
```

tsconfig.json の中身は以下のように書き換えます。

```diff
-    "target": "es2016",
+    "target": "es2020", /* 新しい構文もトランスパイル可能に */

-    "module": "commonjs",
+    "module": "esnext", /* ES Modules も解釈可能に */

-    // "moduleResolution": "node",
+    "moduleResolution": "node", /* npmでインストールしたモジュールも認識可能に */

-    // "outDir": "./",
+    "outDir": "./dist", /* コンパイル結果の出力先を指定 */

-  }
+  },
+  "include": ["./src/**/*.ts"] /* src以下の全てのtsファイルをコンパイル対象に */
```

### Express (エクスプレス)

Express は Node.js で簡単にWEBアプリケーションが作れるフレームワークです。今回は REST API を爆速で作るために使用します。
こちらもインストールします。

```bash
# Express をインストール
$ npm install --save-dev @types/express express
```

### MongoDB

Express では MySQL, Redis, MongoDB など一般的なデータベースが利用できます。ここでは MongoDB を使ってみたいと思います。
MongoDBは NoSQL 系のデータベースで有名なものの1つです。データは JSON 形式で保存するため柔軟かつ直感的な管理ができます。また、RDBのように正規化したりデータの整合性を気にしない分、水平にスケールさせやすいです。

今回は Docker を使って MongoDB の環境を立ててみます。
`docker-compose.yml` ファイルを作って以下の記述をします。
ここで MongoDB の root ユーザとパスワードの設定をしておきます。

```yaml
version: '3.8'

services:
  mongo:
    image: mongo
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - "27017:27017"
```

立ち上げてみます。

```bash
docker-compose up -d
```

コンテナの中に入って確かめてみましょう。

```bash
# コンテナが立ち上がっているか確認
$ docker-compose ps
NAME                              COMMAND                  SERVICE             STATUS              PORTS
sample-express-mongoose-mongo-1   "docker-entrypoint.s…"   mongo               running             0.0.0.0:27017->27017/tcp
# コンテナの中に入る
$ docker exec -it sample-express-mongoose-mongo-1 /bin/sh
# mongosh を使ってDBにログインしてみる。先ほどのユーザ、パスワードを入力する。
$ mongosh -u root
Enter password: password
# データベースを users に切り替える
test> use users
switched to db users
# データベースにusersコレクションを作り値を入れてみる
users> db.users.insertOne({name: "nyanchu"})
{
  acknowledged: true,
  insertedId: ObjectId("638c0fb94ceb083d25f210f6")
}
# コレクションを表示してみる
users> db.users.find()
[ { _id: ObjectId("638c0fb94ceb083d25f210f6"), name: 'nyanchu' } ]
# 終わり
users> quit
```

MongoDB に値を入れて取り出すことができました。
先ほど、データベースやコレクションを作っていないのに use や insert ができたのにお気づきでしょうか。 MongoDB は箱がなくても値を入れたときに自動で作られるという設計になっています。
また、 insert したコレクションに `_id` というのが勝手に付与されているのがわかると思います。 MongoDB ではコレクションに自動的に一意な ID が付与されます。

ここで入れたデータをあとで API から参照してみようと思います。

### Mongoose (マングース)

Express では、 MongoDB の ORM として mongoose というものが人気です。ORMとは、雑に言うとDB操作を簡単にしやすくしてくれるライブラリという感じです。
MongoDB は　JSON 形式のドキュメントなので、コレクションにどんなものでも入ってしまうのですが、mongoose はそこに型制約などを持たせてくれるところが大きな利点です。
早速インストールしてみましょう。

```bash
$ npm install --save-dev mongoose
```

これで必要なパッケージが入りました。

# REST API を作ってみる

では早速インストールしたものを使って REST API を作っていきます。

`src` ディレクトリを作り、その中にプログラムを置いていきます。
ちなみに今こんな感じのディレクトリ構成になっています。

```bash
node_modules/
package.json
docker-compose.yml
package-lock.json
src/
```

「爆速で完成系を動かしたいんだけど！？」という方は 4 をいきなり見てもらえればと思います。コードは簡単なので説明がなくてもなんとなくわかると思います。
「順を追ってやりたいんだけど」という方は 1 から順番に見てください。

### 1. Express で API を作ってみる

まずは Express を使って簡単な API を作ってみます。
`src/app.ts` を作り以下を記述します。簡単な説明がコメントに書いてあります。

```typescript
import express, { Application } from "express";
import mongoose from "mongoose";

// Express のアプリケーションを作成
const app: Application = express();

// 「GET /」のルーティングと処理を記述
app.get("/", (req, res) => {
    res.send("Hello World!!");
});

// Express を立ち上げるポート番号
const EXPRESS_PORT = 3000;

(async function main() {
    try {
        // 指定したポートでリッスンするサーバを立ち上げる
        app.listen(EXPRESS_PORT, () => {
            console.log("server running");
        });
    } catch (e: any) {
        console.error(e.message);
    }
})();

```

実行してみます。ここで先ほどインストールした便利ツール ts-node をさりげなく使っています。
本来は TypeScript は「コンパイル」>「実行」という2段階になるのですが、そこを省略してくれます。

```bash
$ npx ts-node --esm src/app.ts
server running
```

「server running」と表示されました。
`http://localhost:3000/` にアクセスしてみます。

![スクリーンショット 2022-12-04 15.02.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27159/f9f0eef2-cea2-e59d-b8fe-1c37c330b211.png)

「Hello World!!」と表示されましたね！
`app.get("/" ...` とルーティングを記述するだけで簡単に API が作れました。

### 2. MongoDB に Mongoose で接続してみる

さて、続いて MongoDB に Mongoose　で接続するコードを書いてみます。
`src/app.ts` を以下のように書き換えてください。コードのポイントはあとで解説します。

```typescript
import express, { Application } from "express";
import mongoose from "mongoose";

// Express のアプリケーションを作成
const app: Application = express();

// (1) Mongoose のスキーマを作成
const userSchema = new mongoose.Schema(
    {
        name: {
            type: String, // 型を指定する
            required: true, // 必須カラムかどうか
        },
    },
);
const usersResource = mongoose.model("users", userSchema);

// 「GET /users」のルーティングと処理を記述
app.get("/users", async (req, res) => {
    // （2） users から全件取得する
    const users = await usersResource.find();
    res.status(200).json(users);
});

// Express を立ち上げるポート番号
const EXPRESS_PORT = 3000;
// (3) Mongoose のコネクションストリング
const MONGOOSE_URI = "mongodb://root:password@localhost:27017/users";

(async function main() {
    // MongoDB への接続
    await mongoose.connect(MONGOOSE_URI);

    try {
        // 指定したポートでリッスンするサーバを立ち上げる
        app.listen(EXPRESS_PORT, () => {
            console.log("server running");
        });
    } catch (e: any) {
        console.error(e.message);
    }
})();

```

コードのポイントは以下です。コード中に対応した番号も書いてあるので合わせてご覧ください。

* (1) MongoDB の users コレクションを扱うスキーマを作っています。ここでカラムの型や「必須かどうか」などを定義できます。
* (2) MongoDB の users コレクションから検索する部分です。 `find()` で引数を何も指定しないと全件取得になります。
* (3) MongoDB に接続するためには"コネクションストリング"という文字列を指定します。少しクセがありますのでここではおまじないと思ってください。

出来たら再度 `npx ts-node --esm src/app.ts` で実行し、ブラウザで今後は `http://localhost:3000/users` を開いてみます。
以下のように返ってきましたか？
MongoDB に入れた値が API から返ってきていますね！

```json
[{"_id":"638c0fb94ceb083d25f210f6","name":"nyanchu"}]
```

### 3. POST リクエスト処理を書いてみる

続いて POST で json リクエストを受けて、 MongoDB に値を書き込むところを書いてみます。
`src/app.ts` を以下のように書き換えてください。コードはあとで解説します。

```typescript
import express, { Application } from "express";
import mongoose from "mongoose";

// Express のアプリケーションを作成
const app: Application = express();

// Mongoose のスキーマを作成
const userSchema = new mongoose.Schema(
    {
        name: {
            type: String, // 型を指定する
            required: true, // 必須カラムかどうか
        },
    },
);
const usersResource = mongoose.model("users", userSchema);

// （１） リクエストボディを JSON で取得するための設定
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 「GET /users」のルーティングと処理を記述
app.get("/users", async (req, res) => {
    // users から全件取得する
    const users = await usersResource.find();
    res.status(200).json(users);
});
// 「POST /users」のルーティングと処理を記述
app.post("/users", async (req, res) => {
    const body = req.body;
    try {
        // (2) users に書き込む
        await usersResource.create(body);
        res.status(200).send();
    } catch (error: any) {
        res.status(500).json({ message: error.message })
    }
});

// Express を立ち上げるポート番号
const EXPRESS_PORT = 3000;
// Mongoose のコネクションストリング
const MONGOOSE_URI = "mongodb://root:password@localhost:27017/users?authSource=admin";

(async function main() {
    // MongoDB への接続
    await mongoose.connect(MONGOOSE_URI);

    try {
        // 指定したポートでリッスンするサーバを立ち上げる
        app.listen(EXPRESS_PORT, () => {
            console.log("server running");
        });
    } catch (e: any) {
        console.error(e.message);
    }
})();

```

今回のコードのポイントは以下です。

* (1) POST でリクエストボディを JSON で取得するための設定を前段に入れます。
* (2) MongoDB の users コレクションに値を入れる処理です。先ほど程度したスキーマ以外のものを入れようとするとエラーになります。

では実際に試してみます。 `npx ts-node --esm src/app.ts` で再実行するのをお忘れなく。
POST でリクエストしたいので、curl コマンドを使って実行してみます。

```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"name":"sample"}' -i http://localhost:3000/users
HTTP/1.1 200 OK
X-Powered-By: Express
Date: Sun, 04 Dec 2022 06:52:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0
```

200 が返ってきました。成功していそうですね。
では `GET /users` を叩いて中身を確認してみます。

```bash
> curl http://localhost:3000/users
[{"_id":"638c0fb94ceb083d25f210f6","name":"nyanchu"},{"_id":"638c43a4220a7532fbefdc0b","name":"sample","__v":0}]% 
```

全部で2つの値が返ってきました。成功です！

試しに間違った値をリクエストしてみましょう。
今はリクエストした値をそのまま Mongoose に放り込むコードになっているので、 Mongoose が弾いてくれるかどうかが確かめられます。
`{"name": "hoge"}` ではなく `{"title": "hoge"}` をリクエストしてみます。

```bash
> curl -X POST -H "Content-Type: application/json" -d '{"title":"hoge"}' -i http://localhost:3000/users
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 69
ETag: W/"45-8a3w5Br0p00nfvyzxIpNSyLPEXM"
Date: Sun, 04 Dec 2022 09:27:38 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"message":"users validation failed: name: Path `name` is required."}% 
```

見事弾かれました。
`users validation failed: name: Path name is required.` (nameパラメータが必須だよ) と怒られています。
Mongoose の偉大さを感じました。

### 4. CRUD を完成させる

それでは GET, POST に続いて PUT, DELETE も記述し CRUD 完成させます。
`src/app.ts` を以下のように書き換えてください。今回のポイントは後ほど解説します。

```typescript
import express, { Application } from "express";
import mongoose from "mongoose";

// Express のアプリケーションを作成
const app: Application = express();

// Mongoose のスキーマを作成
const userSchema = new mongoose.Schema(
    {
        name: {
            type: String, // 型を指定する
            required: true, // 必須カラムかどうか
        },
    },
);
const usersResource = mongoose.model("users", userSchema);

// リクエストボディを JSON で取得するための設定
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 「GET /users」のルーティングと処理を記述
app.get("/users", async (req, res) => {
    // users から全件取得する
    const users = await usersResource.find();
    res.status(200).json(users);
});
// 「POST /users」のルーティングと処理を記述
app.post("/users", async (req, res) => {
    const body = req.body;
    try {
        // users に書き込む
        await usersResource.create(body);
        res.status(200).send();
    } catch (error: any) {
        res.status(500).json({ message: error.message })
    }
});
// (1) 「PUT /users/{:id}」のルーティングと処理を記述
app.put("/users/:id", async (req, res) => {
    const id = req.params.id
    const body = req.body;
    try {
        // (2) 指定した id に対し更新をかける。 id が見つからなかったら失敗扱いにする。
        await usersResource.findByIdAndUpdate({ _id: id }, body).orFail();
        res.status(200).send();
    } catch (error: any) {
        res.status(500).json({ message: error.message })
    }
});
// 「DELETE /users/{:id}」のルーティングと処理を記述
app.delete("/users/:id", async (req, res) => {
    const id = req.params.id
    try {
        // (3) 指定した id を削除する。 id が見つからなかったら失敗扱いにする。
        await usersResource.findByIdAndDelete({ _id: id }).orFail();
        res.status(200).send();
    } catch (error: any) {
        res.status(500).json({ message: error.message })
    }
});

// Express を立ち上げるポート番号
const EXPRESS_PORT = 3000;
// Mongoose のコネクションストリング
const MONGOOSE_URI = "mongodb://root:password@localhost:27017/users?authSource=admin";

(async function main() {
    // MongoDB への接続
    await mongoose.connect(MONGOOSE_URI);

    try {
        // 指定したポートでリッスンするサーバを立ち上げる
        app.listen(EXPRESS_PORT, () => {
            console.log("server running");
        });
    } catch (e: any) {
        console.error(e.message);
    }
})();

```

今回のコードのポイントは以下です。

* (1) パスパラメータで `:id` を受け付けるには `/users/:id` とルーティングを記述します。その後 `req.params.id` で取得できます。
* (2) `findByIdAndUpdate()` で指定した `id` に対し更新をかけます。もし `id` 自体が見つからなかったときはその後 `.orFail()` をつけておくことで処理を失敗扱いにします。(そのままだとエラーにならないため)
* (3) `findByIdAndDelete()` で指定した `id` を削除します。上記と同じく `id` が見つからなかった場合はその後の `.orFail()` で処理を失敗扱いにします。

出来たので試してみましょう。 `npx ts-node --esm src/app.ts` で再実行するのをお忘れなく。
curl でリクエストしてみます。指定する `id` のところはよしなに変えてください。

```bash
# PUT リクエスト
$ curl -X PUT -H "Content-Type: application/json" -d '{"name":"huga"}' -i http://localhost:3000/users/638c43a4220a7532fbefdc0b
# 試しに GET して確認。書き換わっている！
$ curl http://localhost:3000/users
[{"_id":"638c0fb94ceb083d25f210f6","name":"huga"},{"_id":"638c43a4220a7532fbefdc0b","name":"sample","__v":0}]%

# DELETE リクエスト
$ curl -X DELETE -i http://localhost:3000/users/638c0fb94ceb083d25f210f6
# 試しに GET して確認。消えている！
$ curl http://localhost:3000/users
[{"_id":"638c43a4220a7532fbefdc0b","name":"sample","__v":0}]%
```

これで CRUD が完成しました！

# まとめ

どうでしたでしょうか。爆速で出来ましたか？
Express と Mongoose で手軽にアプリケーションが書けることがわかりました。

今はわかりやすさ重視で全てを1ファイルに書いていますが、ここからコードを大きくしていこうと思ったら以下のような流れでリファクタするのが良いかと思います。

* Mongoose　のスキーマを別ファイルに切り出す
* API のコントローラー部分を別ファイルに切り出す
* API のルーティング部分を別ファイルに切り出す
* Express のリッスンする部分や Mongoose のコネクション部分を分離する（テストしやすくするため）

さらにテストを書くなら

* jest を入れてビジネスロジックのテストを書く
* jest-mongodb を入れて Mongoose 周りのテストを書く
* supertest を入れて Express のコントローラーのテストを書く

ということを考えていきましょう。
余力があればその辺も記事にしていこうと思います。

以上、お付き合いありがとうございました。