Alibaba CloudでMongoDBを使ってNode.jsでREST APIを作成する方法

この記事では、**Alibaba Cloud**上で**MongoDB**を使ってNode.jsで強力なREST APIを作成する方法を探っていきます。

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/blog/create-a-rest-api-in-node-js-with-mongodb-on-alibaba-cloud_593851)*


#Alibaba Cloud ECSにMongoDBをインストールする
MongoDBのインストールは簡単です。


| パッケージ名 | 説明 |
|:--|:--|
| mongodb-org | 以下の4つのコンポーネントパッケージを自動的にインストールするメタパッケージです。 |
| mongodb-org-server | デーモンと関連する設定スクリプトと init スクリプトが含まれています。 |
| mongodb-org-mongos | デーモンが含まれています。|
| mongodb-org-shell | シェルが含まれています。 |
| mongodb-org-tools | 以下の MongoDB ツールが含まれています:  mongoimport bsondump, mongodump, mongoexport, mongofiles, mongorestore, mongostat, and mongotop. |

`mongodb-org-server`パッケージは、`/etc/mongod.conf`設定ファイルでmongodを起動する初期化スクリプトを提供します。

このリンクを使用して、ECS Ubuntu ServerにMongoDBをインストールするには、以下の手順に従ってください: [https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/?spm=a2c65.11461447.0.0.13d5725eN6Tjmv)

インストール後、シンプルなREST APIの構築に進むことができます。

#REST API
RESTとはRepresentational State Transferの頭文字をとったものです。最近ではREST APIが非常に普及しており、Webアプリケーションとモバイルアプリケーションの両方のサーバーサイドを処理するために使用されています。実際、ほとんどの大手インターネット企業では、GoogleカレンダーAPIなど、少なくともいくつかのREST APIを導入しています。データベースが構築されれば、APIを使ってデータやコンテンツを簡単にアプリケーションに配信することができます。これらのAPIは、CRUDプロセスを可能にするために、サーバーがリクエストにどのように応答し、受け入れるかを表しています。このチュートリアルでは、質問を投稿したり、回答を投稿したり、スケーラブルな MongoDB 上で回答を投票したり削除したりするためのシンプルな API を構築します。プロジェクトの実装を成功させるための鍵は、ルートを正しく構造化することです。この場合、ルートは以下のようにプログラムされます。

1、投稿された質問を分析する
2、回答の読み取り、作成、編集、削除
3、回答のアップダウン投票
この例では、以下のようになります。

####開発環境
このプロジェクトでは以下のツールを使用しています。

1、Plain JavaScript
2、Node.js
3、Express (JS framework)
4、MongoDB (データベース)
5、Yarn (パッケージ管理)
6、Visual Studio Code as editor
7、Postman (APIエンドポイントをテストするため)

####依存関係
使用する依存パッケージ

1、Parser (受信エンドポイントリクエストの本文を解析します)
2、Express（アプリケーションを起動）
3、Nodemon（変更を反映させるためにサーバーを再起動する）
4、Mongoose (オブジェクトデータモデルを使って MongoDB のインタラクションを簡素化)
5、Morgan（HTTP リクエストをログに記録するミドルウェア）
6、Eslint with Airbnb (高品質なコードの作成を支援するための拡張機能です)

####チュートリアルのまとめ
この例では、以下の作業を行います。

1、express を使用して API ルートを作成する
2、モデリングAPIデータ
3、Mongooseを使ってMongoDBに連絡する
4、Postmanを使ったAPIのクリーンアップとテスト

#Expressを使ったAPIルートの構築
####基本的なこと
開発を簡単にするには、nodemonをインストールして、パッケージのpackage.jsonにスクリプトを追加します。

1、expressを使って基本的なWebサーバーを作成します（expressにはこのプロセスのチュートリアルがあります）。
2、エクスプレスミドルウェアが柔軟に設定されていることを確認してください。
3、リクエストを処理するためのボディパーサを含む
4、パーサーをエクスプレスミドルウェアに組み込む

####質問のためのルートを作成する
1、サーバーで、ルートを格納するファイルを作成します。
2、POST と GET の両方のルートを使用して質問を検索し、新しい質問を追加します。
3、具体的な質問は、GETルートを作成します。

```
router.get('/', (req, res) => {
  res.json({ response: 'a GET request for LOOKING at questions' });
});
router.post('/', (req, res) => {
  res.json({
    response: 'a POST request for CREATING questions',
    body: req.body
  });
});
router.get('/:qID', (req, res) => {
  res.json({
    response: `a GET request for LOOKING at a special answer id: ${req.params.qID}`
  });
});
```

####回答ルート
1、最初のステップは、Morgan HTTPリクエストロガーをインストールすることです。このパッケージは、あなたのためにリクエストを分析するのに役立ちます。
2、次に、回答を受け入れるためのPOSTルートを作成します。
3、次に、回答の編集と削除を可能にするためにPUTとDELETEをインクルードします。
4、回答のアップ投票とダウン投票のためのPOSTルートを作成します（このチュートリアルでは簡単な投票を使用します）。

```
router.post('/:qID/answers', (req, res) => {
  res.json({
    response: 'a POST request for CREATING answers',
    question: req.params.qID,
    body: req.body
  });
});
router.put('/:qID/answers/:aID', (req, res) => {
  res.json({
    response: 'a PUT request for EDITING answers',
    question: req.params.qID,
    answer: req.params.aID,
    body: req.body
  });
});
router.delete('/:qID/answers/:aID', (req, res) => {
  res.json({
    response: 'a DELETE request for DELETING answers',
    question: req.params.qID,
    answer: req.params.aID,
    body: req.body
  });
});
router.post('/:qID/answers/:aID/vote-:dec', (req, res) => {
  res.json({
    response: 'a POST request for VOTING on answers',
    question: req.params.qID,
    answer: req.params.aID,
    vote: req.params.dec,
    body: req.body
  });
});
```

####エラーハンドラ
どんなアプリケーションでもエラーを回避することはほぼ不可能です。この例では、以下の手順でエラーハンドラを設定します。

1、ミドルウェアは「エラーキャッチャー」として動作します。
2、404エラーをキャッチした後、ミドルウェアを使用して、JSON応答形式で応答するカスタムハンドラにそれを渡します(そうでなければ、以下のように500を使用します)。
3、検証ミドルウェアを設定して、エラーのアップ投票やダウン投票を可能にします。

```
app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.json({
    error: {
      message: err.message
    }
  });
});
```

#Mongooseを使ってMongoDBに接続する
####APIのデータモデルを選択する
MongoDB のストレージに選ばれたデータ型が、正しい構造と関係性を明確に表現していることが非常に重要です。Mongoose は MongoDB へのゲートウェイとして機能します。JSONデータ形式を受け入れるスキーマを作成するために使用します。今回のケースでは、回答のプロパティを持つ質問オブジェクトを使用するのがベストなアプローチです。とはいえ、Mongo ドキュメントにはストレージユニットの最大数があるので、質問に対する回答は無制限ではないことを覚えておきましょう。

####スキーマの作成
1、選択した親子構造に応じて、Mongoose で適切なスキーマを作成します。
2、スキーマを使ってモデルを構築する

```
const AnswerSchema = new Schema({
  text: String,
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
  votes: { type: Number, default: 0 }
});
const QuestionSchema = new Schema({
  text: String,
  createdAt: { type: Date, default: Date.now },
  answers: [AnswerSchema]
});
const Question = mongoose.model('Question', QuestionSchema);
```

####機能にSortとVoteを組み込む
1、最新の回答を優先します
2、回答スキーマに投票の保存オプションを含める
3、Mongoose に、すべての答えを保存しながら事前にソートするように指示します。
4、parentメソッドを使用して、質問の親ドキュメントに回答が参照されるようにします。
5、仕様書で矢印関数の使用を避ける

```
const sortAnswers = (a, b) => {
  if (a.votes === b.votes) {
    return b.updatedAt - a.updatedAt;
  }
  return b.votes - a.votes;
};
QuestionSchema.pre('save', function (next) {
  this.answers.sort(sortAnswers);
  next();
});
AnswerSchema.method('update', function (updates, callback) {
  Object.assign(this, updates, { updatedAt: new Date() });
  this.parent().save(callback);
});
AnswerSchema.method('vote', function (vote, callback) {
  if (vote === 'up') {
    this.votes += 1;
  } else {
    this.votes -= 1;
  }
  this.parent().save(callback);
});
```

#APIをMongoDBにリンクする
あなたは、私の考えでは最も困難な課題に取り組もうとしています。この時点でMongooseのドキュメントをよく見てください。

####エラー処理
1、この場合、paramメソッドを使用してqIDとaIDルートのコールバックを持つことにします。
2、このメソッドは、質問や回答がデータベースで利用できない場合にエラーを識別することができます。

```
router.param('qID', (req, res, next, id) => {
  Question.findById(id, (err, doc) => {
    if (err) return next(err);
    if (!doc) {
      err = new Error('Document not found');
      err.status = 404;
      return next(err);
    }
    req.question = doc;
    return next();
  });
});
router.param('aID', (req, res, next, id) => {
  req.answer = req.question.answers.id(id);
  if (!req.answer) {
    err = new Error('Answer not found');
    err.status = 404;
    return next(err);
  }
  return next();
});
```

####質問ルートの準備
1、これは、Mongo データベース内の質問を GET ルートを使って探します。
2、適切な質問を返します。
3、また、新しい質問を JSON 形式で POST ルートを使ってデータベースに投稿します。
4、1つの質問に対してGETルートを指定します。

```
router.get('/', (req, res, next) => {
  Question.find({}).sort({ createdAt: -1 }).exec((err, questions) => {
    if (err) return next(err);
    res.json(questions);
  });
});
router.post('/', (req, res) => {
  const question = new Question(req.body);
  question.save((err, question) => {
    if (err) return next(err);
    res.status(201);
    res.json(question);
  });
});
router.get('/:qID', (req, res) => {
  res.json(req.question);
});
```

####回答を更新するためのルート
1、回答を作成するためのPOSTの実行は簡単です。このメソッドでは、質問ドキュメントに回答をプッシュし、JSON形式で保存します。
2、新しいJSON入力を返すことで回答を更新できるようにするために、updateメソッドを使用します。
3、したがって、回答を削除するためにDELETEルートを使用する場合は、removeメソッドを使用します。
4、投票の場合は、ミドルウェアに埋め込まれたPOSTルートを使用して、voteメソッドで投票を保存します。
以下の例では、この操作を説明しています。

```
router.post('/:qID/answers', (req, res, next) => {
  req.question.answers.push(req.body);
  req.question.save((err, question) => {
    if (err) return next(err);
    res.status(201);
    res.json(question);
  });
});
router.put('/:qID/answers/:aID', (req, res, next) => {
  req.answer.update(req.body, (err, result) => {
    if (err) return next(err);
    res.json(result);
  });
});
router.delete('/:qID/answers/:aID', (req, res) => {
  req.answer.remove(err => {
    req.question.save((err, question) => {
      if (err) return next(err);
      res.json(question);
    });
  });
});
router.post(
  '/:qID/answers/:aID/vote-:dec',
  (req, res, next) => {
    if (req.params.dec.search(/^(up|down)$/) === -1) {
      const err = new Error(`Not possible to vot for ${req.params.dec}!`);
      err.status = 404;
      next(err);
    } else {
      req.vote = req.params.dec;
      next();
    }
  },
  (req, res, next) => {
    req.answer.vote(req.vote, (err, question) => {
      if (err) return next(err);
      res.json(question);
    });
  }
);
```

これで、REST APIが消費されるようになりました。これでREST APIは消費するための準備が整いました。これから次のフェーズに入ります。

#エンドポイントのクリーンアップとテスト
####APIのテスト
すべてのエンドポイントの機能をテストする最も簡単な方法は、Postmanを使用することです。Chromeの拡張機能を使うこともできますし、デスクトップアプリケーションをダウンロードすることもできます。どちらを選んでも構いませんが、個人的にはChromeの拡張機能が好きです。Postmanは使い方がシンプルで、HTTPテストができます。手動でテストしたくない場合は自動テストを設定することもできます。

####クロスオリジンリソース共有
CORSはセキュリティ上の理由で制限されています。基本的には、ブラウザによるドメインリソースへのアクセスを許可する方法です。そのため、以下のようなステップを経て、ドメインからのAPI消費を可能にするミドルウェアが必要となります。

1、ヘッダはすべてのオリジンへのアクセスを持つ必要があります。
2、HTTP リクエストを有効にする

```
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept'
  );
  if (req.method === 'Options') {
    res.header('Access-Control-Allow-Methods', 'PUT, POST, DELETE');
    return res.status(200).json({});
  }
});
```

####フロントエンドの作業を開始する
あなたのAPIは、あらゆるタイプのフロントエンドと接続できるように設定されています。プロセスは、適切なヘッダーとルートを使用して投稿、取得、配置、削除を行うのと同じくらい簡単です。

#結論
REST APIは、非常にスケーラブルなバックエンドマイクロサービスのセットアップに使える素晴らしいツールです。今回のプロジェクトではMongooseを使って、スケーラブルなオープンソースデータベースであるMongoDB上にシンプルなAPIを実装しました。Alibaba Cloudを使ってMongoDB上でモバイルアプリケーションを動かしてみましょう。この記事をお楽しみいただけたでしょうか。

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*