node.jsとexpressで作るREST API入門ハンズオン勉強会

この資料はnode.jsとexpressで作るREST API入門ハンズオン勉強会用の資料です。    

### アジェンダ

1. node.js HelloWorld
2. express HelloWorld
3. WebAPI（REST API）を作ってみる
----

## 1. node.js HelloWorld

node.js（のーどじぇーえす）は、Google Chromeにも搭載されているJavaScriptエンジンV8をベースとしたJavaScriptアプリケーション実行環境です。    
Google Chromeなどのインターネットブラウザと異なり、CUIのみ提供するため、主にサーバー環境やCUIベースのスクリプトの実行環境として利用されています。

それでは、さっそく、使ってみましょう。

----
### 1-1. インストール

下記サイトからインストーラーをDLしてインストールしてください。    

[https://nodejs.org/ja/](https://nodejs.org/ja/)

2022/07/12現在、v16とv18が選択できますが、ここではv18を選択しましょう。    

インストール後、コンソールから下記のように入力してみてください。

```.sh
node -v
```

バージョン表示されたらインストール成功です。    
あわせて、`npm`もインストールされていることを確認しておきましょう。    

```.sh
npm -v
```
---

### 1-2. HelloWorld

それでは、最初のプログラミングを実施してみましょう。    

テキストエディタを開き、下記コードをコピペ後、任意の場所に、`helloWorld.js` というファイル名で保存してください。    
    
```.js
console.log("hello world");
```

さっそく実行してみます。コンソールから下記のように入力します。

```.sh
node <保存したPath>/helloWorld.js
```

`hello world` と表示されたら成功です！

> **解説**    
> `console.log()`は、コンソール出力用の関数です。      
> ここでは、`hello world` という文字列を表示しています。   
> 試しに、 `console.log(1+1);` という風に書き換えると、`2` と表示されるようになります。

---

### 1-3. 関数

JavaScriptの関数を少しだけ解説します。    
さきほどの`helloworld.js`を書き換えながら、試してみてください。    

他のJavaScriptの文法等の網羅的な解説は割愛しますので、下記サイトなどで学習しておいてください。    

[MDN JavaScript ガイド](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Expressions_and_Operators)

---

#### 1-3-1. 基本形

関数の基本的な書き方は下記の通りです。

```.js
function log(string){
  console.log(string);
}

log("hello world");
```

#### 1-3-2. 関数オブジェクト

下記のように書くこともできます。

```.js
const log = function(string){
  console.log(string);
}

log("hello world");
```

---

#### 1-3-3. アロー関数

さらに下記のように書くこともできます。

```.js
const log = (string) =>{
  console.log(string);
}

log("hello world");
```

#### 1-3-4. コールバック関数

関数に別の関数の呼び出しを指定することができます。
主に時間のかかる処理を呼び出して、処理結果を非同期で得たい、といったシーンで利用します。

```.js
function delay(func){
  setTimeout(()=>{
    func();
  },1000);
}

delay(()=>{
  console.log("done");  
});
```

> **解説**    
> `setTimeout()`は、第2パラメータに指定した数値`ms`後に第1パラメータに指定したコールバック関数の実行を行うシステム関数です。      

---

#### 1-3-5. promise

コールバック関数による非同期処理の実装が多くなると、コードが見通しが悪くなります。    
`Promise`を使うと下記のように書けるようになります。    

```.js
function delay(time){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve();
    },time);
  });
}

delay(1000).then(()=>{
  console.log("done");  // delay(1000)後に実行される
});
```

#### 1-3-6. async関数

`Promise`関数は、下記のように書くこともできます。    

```.js
function delay(time){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve();
    },time);
  });
}

(async ()=>{ // awaitは async functionの中だけで利用可能 (※例外あり)
  await delay(1000);    // awaitで promiseを待つ
  console.log("done");  // delay(1000)後に実行される
})();
```

---

### 1-4. モジュールとパッケージについて

#### 1-4-1. comonJSとESM

プログラミングでは、複数の機能を複数のファイルに（機能単位などで）分けて開発したいことがあります。    
JavaScriptはWebブラウザで実行される、HTMLから呼び出されるモジュールとして発展してきた経緯から「HTMLに複数のjsファイルをロードする」という方法しか提供されていませんでした。    
一方で、node.jsは最初に読み込まれるファイルがjsファイルなので、複数のjsファイルを結合して実行されるモジュール機能が必要になりました。そのような経緯で生まれたのが「commonJS」と呼ばれるモジュール方式です。    

commonJSでは、下記のような方法でモジュールをロードします。    

**module.js**
```.module.js
module.exports = function(){
  return "module";
}
```

**呼び出し側**
```.app.js
let mod1 = require("module.js");
```

node.jsでは、`npm`経由で提供されるパッケージもこのcommonJSの形式で提供され普及したことから、node.jsの多くのプログラミング例として、今でもcommonJSの書式を多く見かけることができます。    

一方で、commonJS形式はブラウザからそのままでは利用できません。    

> `webpack`のようなコードビルドツールを使うことで利用することはできます。    

JavaScriptからJavaScriptをロードしたい、というニーズはWebコンテンツ向けにも広まり、Web標準でも対応されることになりました。   
そこで生まれたのが、「ESModule（ESM）」という形式になります。    

ESMでは、下記のようにモジュールをロードします。    

**module.js**
```.module.js
export default function(){
  return "module";
}
```

**呼び出し側**
```.app.js
import mod1 from "module.js";
```

現在では、node.jsでもESMを利用することができます。    
パッケージについても、最近アップデートされたパッケージはほぼESMに対応しています。   

そして、**現在ではほとんどのブラウザからもESMを利用できます。**

commonJSで書くか、ESMで書くかは選択する必要があります。    
今後このドキュメントでは、全てのモジュールをESMのコードで記載して説明します。

---

#### 1-4-2. ESMをnode.jsから利用する方法

node.js では、いくつかの方法でcommonJSとESMを判定します。
1. ファイル名→拡張子を`.js`もしくは`.cjs`にするとcommonJS、`.mjs`にするとESMとしてロードされます。
2. package.json→`"type":"module"`を追加することで、`.js`もESMとしてロードされるようになります。

#### 1-4-3. toplevel await

先程async関数の説明の際に、「awaitは、async関数の中でのみ利用可能」と説明しました。    
しかし実は例外があります。ESMのトップレベルでもawaitが利用可能になるのです。

```.js
function delay(time){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve();
    },time);
  });
}

(async ()=>{ // awaitは async functionの中だけで利用可能 (※例外あり)
  await delay(1000);    // awaitで promiseを待つ
  console.log("done");  // delay(1000)後に実行される
})();
```

このコードは、ESM（`.mjs`）に置き換える（ファイル名をrenameする）ことで、下記のように即時実行関数`(async()=>{})();` を省略できるようになります。

```.js
function delay(time){
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve();
    },time);
  });
}

await delay(1000);    // awaitで promiseを待つ
console.log("done");  // delay(1000)後に実行される
```

toplevel awaitはブラウザでも下記のように記載すれば同様に使えます。

**index.htmlなど**
```index.html
:
<script type="module">
let aaa = await func1(); // toplevel await可能
:
</script>

```

---

## 2. express HelloWorld

expressは、node.js上でWebアプリケーションを開発するための小さなフレームワークです。    
いわゆるWebアプリケーションフレームワークというと、フルスタックなフレームワークを想像しがちですが、expressは（一応htmlをテンプレート処理するejsという機能も含まれてはいるものの）基本的にはバックエンドのミドルウエアにフォーカスした機能が提供されています。    

[https://expressjs.com](https://expressjs.com)

それでは、さっそく使ってみましょう。

---

### 2-1. 準備

これまでは、`helloworld.js`にひたすらコードを書き換える、ということのみで説明を続けてきましたが、今後複数のパッケージを利用するアプリケーションを作っていくことになりますので、ここで node.jsのアプリケーションパッケージを作成してみましょう。   

コンソールから、下記のように入力してください。`任意のpath` は、お好きなもので。    

```.sh
cd `任意のpath`
mkdir helloapp
cd helloapp
npm init
```

---

対話形式で、いろいろ聞かれます。下記内容で入力をお願いします。
- package name: (helloapp) →エンターキー
- version: (1.0.0) →エンターキー
- description: →エンターキー
- entry point: (index.js) →index.mjs
- test command: →エンターキー
- git repository: →エンターキー
- keywords: →エンターキー
- author: →エンターキー
- license: (ISC) →エンターキー
- Is this OK? (yes) →→エンターキー

(公開するアプリを作る場合は、もっとまじめにいれましょう。)    

```package.json
{
  "name": "helloapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.mjs",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
```

上記内容で`package.json`ファイルが出来上がったはずです。

---

それでは、express をインストールします。(expressはアプリケーションパッケージにインストールされます)

```.sh
npm i express -S
```

---

### 2-2. 最小のアプリケーション

続いて、`index.mjs` というファイル名で、先程作成した`package.json`と同じディレクトリに下記内容のファイルを作成します。

```.js
import express from "express";
import http from 'http';

const app = express();

app.get('/hello',(req,res)=>{
  res.send("hello server");
});

const webServer = http.createServer(app);
webServer.listen(3000,()=>{
  console.log("server running PORT:"+3000);
});
```

#### 2-2-1. コード解説

- `import express from "express";` ：expressモジュールをロードしています。    
- `import http from 'http';` ：HTTPモジュール（Webサーバー機能）をロードしています。node.jsには標準でHTTPパッケージが含まれているため、パッケージのインストールは不要です。
- `const app = express();` ：expressのインスタンスを生成しています。
- `app.get('/hello'...);` ：`/hello`というルートの振る舞いを実装しています。ここでは、`/hello` に対する `GET`リクエストのみに応答する処理（`hello server`という応答を返すだけ）を行っています。
- `const webServer = http.createServer(app)`：HTTPモジュールを使ってWebサーバーのインスタンスを生成しています。`app`というexpressで生成したインスタンスを渡すことで、サーバーへのリクエストと`app`インスタンスで定義した処理を結合しています。
- `webServer.listen()`：Webサーバーを起動しています。ここでは、`localhost（default）`のPORT`3000`で待ち受ける設定を行っています。第2パラメータは、コールバック関数で起動後のLOG表示を行っています。

---

#### 2-2-2. 実行とテスト

コードができたので、下記コマンドで実行してみます。

```.sh
node ./index.mjs
```

コンソールに、`server running PORT:3000` と表示されたら起動成功です。
これで、`http://localhost:3000` にサーバーが起動しました。

ブラウザを起動し、URLバーに上記URLを入力してアクセスしてみましょう。    
`Cannot GET /` と表示されたら、ブラウザからのアクセスは成功しています。    

正しい結果が得られていない？と思うのは、アクセスするURLが間違っているからです。    
今回、`app.get('/hello',...);` とコードを書きましたので、この部分の処理結果をテストするためには、`http://localhost:3000/hello` にアクセスする必要があります。

このURLにアクセスすると `hello server` と表示されたはずです。    

---

### 2-3. 機能を追加してみる①

先程作った「最小のアプリケーション」をベースに、少しずつ機能を追加していきましょう。    

```diff
import express from "express";
import http from 'http';

const app = express();

app.get('/hello',(req,res)=>{
  res.send("hello server");
});

+ // ココにコードを追加する

const webServer = http.createServer(app);
webServer.listen(3000,()=>{
  console.log("server running PORT:"+3000);
});
```

先程作ったアプリケーションは、`GET /hello` というリクエストに対応するアプリケーションでした。この`GET /hello`を「ルートパス」と言います。

ここで、`GET /countup`というルートパスを追加してみます。
上記`ココにコードを追加する`と記載した箇所に、下記コードを挿入してみてください。     


```.js
let counter = 0;
app.get('/countup',(req,res)=>{
  counter ++;
  res.send(""+counter);
});
```

> **解説**    
> `counter`というグローバル変数を`/countup`が呼ばれた際にカウントアップしています。     
> `res.send(""+counter);`のパラメータ部分の`""+`は、Number型のオブジェクトを「数字の文字列」に変換しています。（HTMLレスポンスをテキストデータに変換するため）

先程起動したアプリケーションを一旦強制終了（コンソールから`ctrl+c`など）させ、再度、`node ./index.mjs` で起動します。    

ブラウザから、`http://localhost:3000/countup` にアクセスすると、アクセスした回数に応じた数字が表示されるはずです。  

---  

### 2-4. 機能を追加してみる②

続けて機能を追加してみます。    
現在作成しているアプリケーションは、ルートパス`/hello`にアクセスすると毎回必ず`hello server`と返しています。    
これを下記のように書き換えてみましょう。    

- `POST /post-hello` にアクセスすると、`GET /hello`にアクセスした時に表示される文字列を書き換える。
- `GET /hello` にアクセスすると、`/post-hello` で指定された文字列を表示する。ただし`POST /post-hello`が一度も呼び出されていない時は`hello server`と表示する。

それでは、コードを書き換えてみましょう。    

```diff
import express from "express";
import http from 'http';

const app = express();

-app.get('/hello',(req,res)=>{
-  res.send("hello server");
-});

let counter = 0;
app.get('/countup',(req,res)=>{
  counter ++;
  res.send(""+counter);
});

const webServer = http.createServer(app);
webServer.listen(3000,()=>{
  console.log("server running PORT:"+3000);
});
```

上記`-`の3行の部分に下記コードで上書きします。

```.js
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

let helloMessage = "hello server";
app.get('/hello',(req,res)=>{
  res.send(helloMessage);
});

app.post('/post-hello',(req,res)=>{
  if(req.body.message){
    helloMessage = req.body.message;
    res.send("OK:"+helloMessage);
  }else{
    res.send("ERROR");
  }
});
```

アプリケーションを一旦強制終了（コンソールから`ctrl+c`など）させ、再度、`node ./index.mjs` で起動しておいてください。    

---

### 2-5. POSTリクエストを発行する

さて、今まではブラウザだけでテストしていましたが、ブラウザのアドレスバー経由でテストできるのは`GET`リクエストだけです。    
今回データを投入するために`POST`リクエストを作成しましたので、`POST` のテストはブラウザではなく`curl`を利用することにします。    

curlは、HTTPリクエストを発行するコマンドラインツールです。    

参考：[curl コマンド 使い方メモ](https://qiita.com/yasuhiroki/items/a569d3371a66e365316f)

- windows: windows 10では最初から使えるようです。
- mac: 最初から使えます。
- ubuntu: `sudo apt install -y curl` でインストールできます。

サーバーを起動後、サーバーを起動したのとは別のWindowでターミナル（コマンドプロンプト）を起動し、下記コマンドを入力してみてください。

```.sh
curl -X POST -H "Content-Type:application/json" http://localhost:3000/post-hello -d "{\"message\":\"hellohello\"}"
```

コンソールに `OK:hello` と表示されたら成功です。    

パラメーターエラーのテストも行ってみましょう。    
`POST /post-hello`は、リクエストbodyに`message`が含まれていないとエラーと判定していますので、`message`を`mes`に変更してみます。

```.sh
curl -X POST -H "Content-Type:application/json" http://localhost:3000/post-hello -d "{\"mes\":\"hellohello\"}"
```

`ERROR` と表示されましたので、パラメーターエラーの動作も確認することができました。    

これで、`POST /post-hello`の動作が確認できましたので、`POST /post-hello`後に、指定したメッセージに書き換えられた応答が得られるか、`GET /hello`へのリクエスト結果も確認しておきましょう。    

`GET /hello`は、これまで通りブラウザのURLバー経由でも動作確認できますが、`curl`でも下記のように入力すれば動作確認できます。

```
curl http://localhost:3000/hello
```

書き換えたメッセージが表示されたら成功です。    

---

## 3. WebAPI（REST API）を作ってみる

これまでに、下記のようなアプリケーションができました。

- `GET /hello`にリクエストすると`POST /post-hello`で設定したメッセージを返す。
- `POST /post-hello`にリクエストするとリクエストbodyの`message`パラメータで指定したメッセージを保存する。
- `GET /countup`にリクエストするとサーバーに保持したカウンターをカウントアップし、カウントアップ後の値を返す。

なんとなく、WebAPIっぽくなってきましたが、実はまだいろいろ足りません。    
次のステップで、このアプリケーションをWebAPI（REST API）に書き換えてみます。    

---

### 3-1. RESTとは

RESTとはREpresentational State Transferの略で、Webアプリケーションの設計指針のことを指します。

RESTの原則は下記の通りです。    

1. URLにはリソースを定義する
2. HTTPメソッド（GET/POST/PUT/DELETE）を利用する
3. ステートレスである
4. 処理結果をHTTPステータスコードで通知する

上記設計指針に従って設計されたWebAPIをREST（RESTful） APIと呼びます。

---

#### 3-1-1. URLにはリソースを定義する

リソースですので、名詞で定義することになります。    
`getName`みたいに動詞と組み合わせるのはRESTの指針には合いません。
また、リソースは階層的に表現します。    
例えば、`name`というリソースであれば、それがどのようなリソースの一部なのか、と考えます。ユーザー情報の一部として「name」を扱う場合には、`/users/<userid>/name`のようになります。    

---

#### 3-1-2. HTTPメソッド（GET/POST/PUT/DELETE）を利用する

先程の`name`というリソースに対して下記のようなHTTPメソッド（操作）を提供することができます。

|HTTPメソッド|機能|
|:---:|:---|
|`GET`|取得する|
|`POST`|登録する|
|`PUT`|更新する|
|`DELETE`|削除する|

先程の`/users/<userid>/name`の例ですと下記のようになります。    

|HTTPメソッド|機能|
|:---:|:---|
|`GET /users/kodama/name`|ユーザー「kodama」の「名前」情報を取得する|
|`POST /users/kodama/name`|ユーザー「kodama」の「名前」情報を登録する|
|`PUT /users/kodama/name`|ユーザー「kodama」の「名前」情報を更新する|
|`DELETE /users/kodama/name`|ユーザー「kodama」の「名前」情報を削除する|

---

#### 3-1-3. ステートレスである

ステートレスとは、状態を持たない、ということです。    
`GET`や`POST`という要求の完了とともに、処理も完了し、「処理中」のような状態は持ちません。    
各メソッド（操作）に必要な情報は、都度伝達できるように設計する必要があります。    

下記サイトの説明がわかりやすいので、少し読んでみてください。

http://yohei-y.blogspot.com/2007/10/blog-post.html

ステートレスな設計は、アプリケーションをシンプルに設計する上で基本的な考えではありますが、要件によっては状態管理が必要なシーンもでてきます。一部ステートフルなメソッドを設計したからといってREST APIではない、ということにはなりません。あくまでガイドラインですので、基本をステートレス側に倒す、と捉えておいてください。    

---

#### 3-1-4. 処理結果をHTTPステータスコードで通知する

HTTPステータスコードとは、HTTPレスポンスヘッダに含まれるリクエスト処理結果を表す3桁のコードで、下記のように内容が決まっています。    

https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

REST APIでは処理成功、失敗を下記のようにステータスコードをマッピングすることが多いです。
（あくまで例であり、操作が提供する機能により最も適切なステータスコードを設定する、という方針です）    

|要求|処理結果|ステータスコード|
|:---:|:---:|:---:|
|`GET`|成功|200|
|`POST`|成功|200or201|
|`PUT`|成功|200or201|
|`DELETE`|成功|200or204|

---

### 3-2. REST APIに変更してみよう（練習）

それでは、下記コードをベースに、下記機能を持つREST APIを設計してプログラミングしてみましょう。    

```.js
import express from "express";
import http from 'http';

const PORT_NUMBER = 3000;

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// このヘんにルートパスのコードを書く

const webServer = http.createServer(app);
webServer.listen(PORT_NUMBER,()=>{
  console.log("web server running PORT:"+PORT_NUMBER);
});
```

- `message` というリソースを操作する機能を提供する。`message`は何かのリソースの一部ではなく、トップリソースとして定義する。また`message`リソースはアプリケーション中で単一（singleton）である。
- 全ての操作へのレスポンスはJSONを返却する。
- `GET` で、最新の`message`を取得できる。成功時のステータスコードは`200`、失敗は考慮しない。
- `POST` で、`message`の文字列を登録/更新できる（ここでは文字数制限等は考慮しない）。パラメーターは、jsonでリクエストbodyに`text`オブジェクトで指定する。成功時のステータスコードは常に`201`、パラメーターエラー時のみ失敗することとし、その時のステータスコードは`401`とする。
- `DELETE` で、`message`を初期値に初期化できる。初期値は `hello` とする。成功時のステータスコードは`200`、失敗は考慮しない。（既に`hello`だった時も成功とする）

完成したら、`curl`を使ってテストしてみましょう。

以下、これまで説明してないことの補足です。

---

#### 3-2-1. ステータスコードの変更方法

これまで、expressでステータスコードを指定してレスポンスする方法を説明しておりませんでした。

下記コードでは、常に`200`が返却されます。

```.js
res.send(data);
```

ステータスコード`201`を返却するには、下記のように記載します。

```.js
res.status(201).send(data);
```

---

#### 3-2-2. JSONでレスポンスする方法

`res.send()`は、パラメータに渡したオブジェクトを内部でJSONにシリアライズしています。   
このため、例えば `{"name":"kodama"}` というJSONデータを送信したい場合、下記のように書くことができます。

```
const resobject = {name:"kodama"};
res.send(resobject);
```

expressでは、レスポンスの返却用に`res.json()`というメソッドも用意されています。レスポンスBODYにJSONデータをレスポンスする、という挙動において、`res.send()`と同じ挙動を行うようです。    

`res.send()`と`res.json()`の違いはレスポンスヘッダのmime-typeです。

|メソッド|デフォルトmime-type|
|:---:|:---:|
|`res.send`|`text/html`|
|`res.json`|`application/json`|

JSONの正しいmime-typeは`application/json`なので、今回は`res.json()`を使いましょう。

---

#### 3-2-3. DELETEリクエストを作成する方法

`DELETE`リクエストは、下記のように`app.delete()`を用いてルートパスを作成します。    

```.js
app.delete('/message',(req,res)=>{
  // 処理
});
```
curlでテストの際は、下記のように`-X DELETE` を指定するようにしてください。    

```.sh
curl -X DELETE http://localhost:3000/resource
```

---

おつかれさまでした！

#### リンク集

資料執筆にあたり下記サイトの情報（以下敬称略、順不同）を参考にさせていただきました。多謝！

- [MDN『JavaScript ガイド』](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Expressions_and_Operators)
- [node.js](https://nodejs.org/ja/)
- [express ガイド(日本語) ルーティング](https://expressjs.com/ja/guide/routing.html)
- [Qiita @kanataxa『GETとPOSTの違いについて』](https://qiita.com/kanataxa/items/522efb74421255f0e0a1)
- [Qiita @yasuhiroki(A10 Lab Inc.)『curl コマンド 使い方メモ』](https://qiita.com/yasuhiroki/items/a569d3371a66e365316f)
- [Wiz テックブログ 『REST APIとは？ざっくりと理解してみる【初心者向け】』](https://tech.012grp.co.jp/entry/rest_api_basics)
- [IT用語辞典 e-words『シングルトン【singleton】』](https://e-words.jp/w/%E3%82%B7%E3%83%B3%E3%82%B0%E3%83%AB%E3%83%88%E3%83%B3.html)
- [Neo's World『Express のレスポンス関連メソッド「res.end()」「res.send()」「res.json()」の違い』](https://neos21.net/blog/2019/03/12-01.html)
- [wikipedia『HTTPステータスコード』](https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)
- [yohei-y:weblog『ステートレスとは何か』](http://yohei-y.blogspot.com/2007/10/blog-post.html)

#### ライセンス

[CCBY4.0](https://creativecommons.org/licenses/by/4.0/deed.ja) [@tadfmac (Hideyuki Kodama)](https://github.com/tadfmac)