この資料はnode.jsとexpressで作るREST API入門ハンズオン勉強会用の資料です。
アジェンダ
- node.js HelloWorld
- express HelloWorld
- WebAPI(REST API)を作ってみる
1. node.js HelloWorld
node.js(のーどじぇーえす)は、Google Chromeにも搭載されているJavaScriptエンジンV8をベースとしたJavaScriptアプリケーション実行環境です。
Google Chromeなどのインターネットブラウザと異なり、CUIのみ提供するため、主にサーバー環境やCUIベースのスクリプトの実行環境として利用されています。
それでは、さっそく、使ってみましょう。
1-1. インストール
下記サイトからインストーラーをDLしてインストールしてください。
2022/07/12現在、v16とv18が選択できますが、ここではv18を選択しましょう。
インストール後、コンソールから下記のように入力してみてください。
node -v
バージョン表示されたらインストール成功です。
あわせて、npm
もインストールされていることを確認しておきましょう。
npm -v
1-2. HelloWorld
それでは、最初のプログラミングを実施してみましょう。
テキストエディタを開き、下記コードをコピペ後、任意の場所に、helloWorld.js
というファイル名で保存してください。
console.log("hello world");
さっそく実行してみます。コンソールから下記のように入力します。
node <保存したPath>/helloWorld.js
hello world
と表示されたら成功です!
解説
console.log()
は、コンソール出力用の関数です。
ここでは、hello world
という文字列を表示しています。
試しに、console.log(1+1);
という風に書き換えると、2
と表示されるようになります。
1-3. 関数
JavaScriptの関数を少しだけ解説します。
さきほどのhelloworld.js
を書き換えながら、試してみてください。
他のJavaScriptの文法等の網羅的な解説は割愛しますので、下記サイトなどで学習しておいてください。
1-3-1. 基本形
関数の基本的な書き方は下記の通りです。
function log(string){
console.log(string);
}
log("hello world");
1-3-2. 関数オブジェクト
下記のように書くこともできます。
const log = function(string){
console.log(string);
}
log("hello world");
1-3-3. アロー関数
さらに下記のように書くこともできます。
const log = (string) =>{
console.log(string);
}
log("hello world");
1-3-4. コールバック関数
関数に別の関数の呼び出しを指定することができます。
主に時間のかかる処理を呼び出して、処理結果を非同期で得たい、といったシーンで利用します。
function delay(func){
setTimeout(()=>{
func();
},1000);
}
delay(()=>{
console.log("done");
});
解説
setTimeout()
は、第2パラメータに指定した数値ms
後に第1パラメータに指定したコールバック関数の実行を行うシステム関数です。
1-3-5. promise
コールバック関数による非同期処理の実装が多くなると、コードが見通しが悪くなります。
Promise
を使うと下記のように書けるようになります。
function delay(time){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve();
},time);
});
}
delay(1000).then(()=>{
console.log("done"); // delay(1000)後に実行される
});
1-3-6. async関数
Promise
関数は、下記のように書くこともできます。
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.exports = function(){
return "module";
}
呼び出し側
let mod1 = require("module.js");
node.jsでは、npm
経由で提供されるパッケージもこのcommonJSの形式で提供され普及したことから、node.jsの多くのプログラミング例として、今でもcommonJSの書式を多く見かけることができます。
一方で、commonJS形式はブラウザからそのままでは利用できません。
webpack
のようなコードビルドツールを使うことで利用することはできます。
JavaScriptからJavaScriptをロードしたい、というニーズはWebコンテンツ向けにも広まり、Web標準でも対応されることになりました。
そこで生まれたのが、「ESModule(ESM)」という形式になります。
ESMでは、下記のようにモジュールをロードします。
module.js
export default function(){
return "module";
}
呼び出し側
import mod1 from "module.js";
現在では、node.jsでもESMを利用することができます。
パッケージについても、最近アップデートされたパッケージはほぼESMに対応しています。
そして、現在ではほとんどのブラウザからもESMを利用できます。
commonJSで書くか、ESMで書くかは選択する必要があります。
今後このドキュメントでは、全てのモジュールをESMのコードで記載して説明します。
1-4-2. ESMをnode.jsから利用する方法
node.js では、いくつかの方法でcommonJSとESMを判定します。
- ファイル名→拡張子を
.js
もしくは.cjs
にするとcommonJS、.mjs
にするとESMとしてロードされます。 - package.json→
"type":"module"
を追加することで、.js
もESMとしてロードされるようになります。
1-4-3. toplevel await
先程async関数の説明の際に、「awaitは、async関数の中でのみ利用可能」と説明しました。
しかし実は例外があります。ESMのトップレベルでもawaitが利用可能になるのです。
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()=>{})();
を省略できるようになります。
function delay(time){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve();
},time);
});
}
await delay(1000); // awaitで promiseを待つ
console.log("done"); // delay(1000)後に実行される
toplevel awaitはブラウザでも下記のように記載すれば同様に使えます。
index.htmlなど
:
<script type="module">
let aaa = await func1(); // toplevel await可能
:
</script>
2. express HelloWorld
expressは、node.js上でWebアプリケーションを開発するための小さなフレームワークです。
いわゆるWebアプリケーションフレームワークというと、フルスタックなフレームワークを想像しがちですが、expressは(一応htmlをテンプレート処理するejsという機能も含まれてはいるものの)基本的にはバックエンドのミドルウエアにフォーカスした機能が提供されています。
それでは、さっそく使ってみましょう。
2-1. 準備
これまでは、helloworld.js
にひたすらコードを書き換える、ということのみで説明を続けてきましたが、今後複数のパッケージを利用するアプリケーションを作っていくことになりますので、ここで node.jsのアプリケーションパッケージを作成してみましょう。
コンソールから、下記のように入力してください。任意のpath
は、お好きなもので。
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) →→エンターキー
(公開するアプリを作る場合は、もっとまじめにいれましょう。)
{
"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はアプリケーションパッケージにインストールされます)
npm i express -S
2-2. 最小のアプリケーション
続いて、index.mjs
というファイル名で、先程作成したpackage.json
と同じディレクトリに下記内容のファイルを作成します。
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. 実行とテスト
コードができたので、下記コマンドで実行してみます。
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
というルートパスを追加してみます。
上記ココにコードを追加する
と記載した箇所に、下記コードを挿入してみてください。
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行の部分に下記コードで上書きします。
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リクエストを発行するコマンドラインツールです。
- windows: windows 10では最初から使えるようです。
- mac: 最初から使えます。
- ubuntu:
sudo apt install -y curl
でインストールできます。
サーバーを起動後、サーバーを起動したのとは別のWindowでターミナル(コマンドプロンプト)を起動し、下記コマンドを入力してみてください。
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
に変更してみます。
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の原則は下記の通りです。
- URLにはリソースを定義する
- HTTPメソッド(GET/POST/PUT/DELETE)を利用する
- ステートレスである
- 処理結果を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
という要求の完了とともに、処理も完了し、「処理中」のような状態は持ちません。
各メソッド(操作)に必要な情報は、都度伝達できるように設計する必要があります。
下記サイトの説明がわかりやすいので、少し読んでみてください。
ステートレスな設計は、アプリケーションをシンプルに設計する上で基本的な考えではありますが、要件によっては状態管理が必要なシーンもでてきます。一部ステートフルなメソッドを設計したからといって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を設計してプログラミングしてみましょう。
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
が返却されます。
res.send(data);
ステータスコード201
を返却するには、下記のように記載します。
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()
を用いてルートパスを作成します。
app.delete('/message',(req,res)=>{
// 処理
});
curlでテストの際は、下記のように-X DELETE
を指定するようにしてください。
curl -X DELETE http://localhost:3000/resource
おつかれさまでした!
リンク集
資料執筆にあたり下記サイトの情報(以下敬称略、順不同)を参考にさせていただきました。多謝!
- MDN『JavaScript ガイド』
- node.js
- express ガイド(日本語) ルーティング
- Qiita @kanataxa『GETとPOSTの違いについて』
- Qiita @yasuhiroki(A10 Lab Inc.)『curl コマンド 使い方メモ』
- Wiz テックブログ 『REST APIとは?ざっくりと理解してみる【初心者向け】』
- IT用語辞典 e-words『シングルトン【singleton】』
- Neo's World『Express のレスポンス関連メソッド「res.end()」「res.send()」「res.json()」の違い』
- wikipedia『HTTPステータスコード』
- yohei-y:weblog『ステートレスとは何か』