17
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-18
1 / 31

この資料は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/

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 ガイド


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

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


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)のPORT3000で待ち受ける設定を行っています。第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. 機能を追加してみる①

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

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と表示する。

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

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 コマンド 使い方メモ

  • 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が含まれていないとエラーと判定していますので、messagemesに変更してみます。

.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. ステートレスである

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

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

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


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

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

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

おつかれさまでした!

リンク集

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

ライセンス

CCBY4.0 @tadfmac (Hideyuki Kodama)

17
15
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
17
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?