no-image

Node.jsとExpressでWeb APIを作ってみよう

GoogleやAmazon、Twitterなどの会社は、自社サービスのAPIを公開しており、だれでも自由に使えるようになっています。APIを通すことでそのサービスの機能にアクセスでき、プログラムからの操作が可能になります。

ウェブサービスのプログラミングをしていると、ときにはAPIを利用する側ではなく、APIを作る側に回ることがあります。しかし、APIを作るのに慣れ親しんでいる人は、あまりいないと思います。

そこでこの記事では、Node.jsとExpressで簡単なWeb APIを作ってみて、「どんな感じなんだろう」「どうすればいいんだろう」といったことを掴んでもらいたいと思います。

Web APIとは

APIってなんだろう

Web APIを作る前に、そもそもWeb APIとはなんでしょうか。「Web」の「API」ということはわかりますが、「API」を具体的に説明しろと言われると、ちょっと言葉に詰まりますね。

APIとはApplication Programming Interfaceの略で、プログラム同士がやり取りするためのインターフェイス(決まりごと)の意味です。ちょっとふわふわした意味だな、と思ったかもしれませんが、実際にも「API」という言葉がさす意味はだいぶ広く、その場その場によって細かい意味は変わります。

APIの一例を出すならば、「ライブラリのメソッドAを実行するとXが起こる」という決まりごともAPIと言えます。たとえばJavaScriptでいえばFetch API(参考:JavaScriptのFetch APIを利用してリクエストを送信する)などがありますね。Fetch APIはfetch()関数を実行するとリソースを取得してくれます。この「fetch関数を実行するとリソースを取得する」という決まりごとがAPIです。

APIはやり取りの決まりごとなので、もちろんプログラムだけでなく通信にも適用できます。たとえば「https://example.com/kfurumiyaというURLにアクセスすると、古都ことのプロフィールが取得できる」という決まりごとがあれば、それはAPIですね。こんな感じで「https://example.com/timelineにアクセスするとタイムラインのデータが取得できる」「https://example.com/tweetにツイートを送信するとツイートできる」のような決まりごとがあれば、それがAPIになります。こういったものはWebを使ったAPIなので、Web APIとも呼ばれます。

つまり「あるURLには、ある機能が与えられている」というのが決まっていたら、それがWeb APIとなります。

Web APIは普通は部分的な値を返す

では、普通のホームページも「Web API」になる?

だって「https://google.com/」にアクセスするとGoogleが開くという決まりごとがあるし。

うーん、正解であるんですけど、でも、実はただのホームページのことはWeb APIと言うことは少ないです。Web APIというときはHTMLやCSSを返すというより、JSONなどで部分的な値を返すものと一般的には認識されています。

加えてWeb APIというときは暗に「サーバで動的に情報を処理する」という意味も含まれています。つまりHTMLファイルを配信するだけのホームページは、一般的にはAPIとはいいません。

そうすると、「アクセスすると部分的に情報を返してくれる」「送信すると情報を処理してくれる」あたりがWeb APIになりますね。

Web APIを作ってみよう

前提条件

  • Node.jsおよびnpmがインストールされている
  • JavaScriptのことがある程度わかる

Express

Node.jsにはExpressという有名なモジュールがあります。Expressを使えば、簡単にウェブサーバを立てることができます。静的ファイルを配信するだけの簡単なウェブサーバから、APIを持つような複雑なサーバまで自由自在です。

今回はこのExpressを使うことにします。

この記事のソースコード

この記事のソースコードはすべてGitHubに上げてあります。ご自由にお使いください。

https://github.com/subterraneanflowerblog/mywebapi

作り始めよう(01_hello)

それでは実際にWeb APIを作ってみましょう。

まずフォルダを作ります。名前は「mywebapi」とします。この中で作業していくとしましょう。

mkdir mywebapi
cd mywebapi

中に入ったら、npm initでプロジェクトを初期化します。initのときにいろいろ聞かれますが、全部何も入力せずにそのままEnterを押せばいいです。

npm initが済んだら、expressをインストールします。

npm init
npm install --save express

expressをインストールしたら、実際にウェブサーバを作ります。

mywebapiフォルダの中に「index.js」というファイルを作成してください。そして以下の内容を書き込んで保存します。

// expressモジュールを読み込む
const express = require('express');

// expressアプリを生成する
const app = express();

// ルート(http://localhost/)にアクセスしてきたときに「Hello」を返す
app.get('/', (req, res) => res.send('Hello'));

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

書き込めたら、このプログラム起動します。起動方法は、「node index.js」です。

node index.js

そうすると「Listening on port 3000」というメッセージが表示されるので、そのままブラウザを開いて「http://localhost:3000/」にアクセスしてみてください。

「Hello」という文字が表示されているはずです!

確認できたら、いったんサーバを停止させましょう。このサーバを停止させるには、Ctrl+Cを押します。

これはもっとも簡単なAPIとなります。http://localhost:3000/にアクセスすると、Helloが返ってくる、というAPIです。これだけでは意味が全くありませんが、ここから発展させていくことで、より複雑なことが実現できるようになります。

今回のキモとなるのは、index.jsの以下の部分です。

// ルート(https://localhost/)にアクセスしてきたときに「Hello」を返す
app.get('/', (req, res) => res.send('Hello'));

これはサーバの「/」にGETメソッドでアクセスしてきたときに、Helloという文字列を返す、というコードになります。GETメソッドというのは、要は普通のアクセスです。ブラウザでURLを入力して開くとGETメソッドでのアクセスになります。

引数のreq, resはそれぞれリクエスト(相手から送られてきた情報)とレスポンス(サーバから送る情報)になります。resオブジェクトのsendメソッドを実行することで、相手にデータを送り返すことができます。

これから、この部分を変更していくことで、より本格的なAPIを構成していきます。

JSONを返してみよう(02_json)

単なるテキストを返すよりも、JSONを返せた方が活用の幅は広いはずです。今度はJSONを返すAPIを作ってみましょう。

ExpressでJSONを返すには、res.sendの部分を、res.jsonに変えるだけです。そして今回は…そうですね、TODOリストでも返してみましょう。

index.jsを書き換えます。

// expressモジュールを読み込む
const express = require('express');

// expressアプリを生成する
const app = express();

// http://localhost:3000/api/v1/list にアクセスしてきたときに
// TODOリストを返す
app.get('/api/v1/list', (req, res) => {
    // クライアントに送るJSONデータ
    const todoList = [
        { title: 'JavaScriptを勉強する', done: true },
        { title: 'Node.jsを勉強する', done: false },
        { title: 'Web APIを作る', done: false }
    ];

    // JSONを送信する
    res.json(todoList);
});

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

送信するJSONデータを用意して、res.jsonで送るだけです。

そしてURLも凝ってみました。Web APIは、一般的にはhttp://example.com/api/v1/xxxのように、「/api/APIバージョン/機能名」の形式を取ることが多いです。なのでそれにあわせて、/api/v1/listとしてみました。

これを保存できたらサーバを立ち上げます。サーバが動きっぱなしの場合はCtrl+Cで一度止めてからもう一度立ち上げてください。

node index.js

そしてブラウザで、http://localhost:3000/api/v1/listにアクセスします。

JSONデータが帰ってきていますね!

これで「/api/v1/listにGETメソッドでアクセスしてきたときにTODOリストをJSONで返す」APIができました。かなり単純なものですが、立派なAPIです!

JSONを描画してみよう(03_render_json)

APIというものは基本的にはテキストやJSONを返してくるもので、描画まで面倒を見てくれるわけではありません。そこで今度はAPIからJSONデータを取得して、それをもとにUIを描画する、ということをやってみましょう。

今回はAPIはいじりません。まず、Expressを使ってHTMLを公開します。

// expressモジュールを読み込む
const express = require('express');

// expressアプリを生成する
const app = express();

// webフォルダの中身を公開する
app.use(express.static('web'));

// http://localhost:3000/api/v1/list にアクセスしてきたときに
// TODOリストを返す
app.get('/api/v1/list', (req, res) => {
    // クライアントに送るJSONデータ
    const todoList = [
        { title: 'JavaScriptを勉強する', done: true },
        { title: 'Node.jsを勉強する', done: false },
        { title: 'Web APIを作る', done: false }
    ];

    // JSONを送信する
    res.json(todoList);
});

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

8行目が追加部分です。この部分により、プロジェクトフォルダの下の「web」という名前のフォルダの中身が公開されます。

次に「web」というフォルダを作り、その中にindex.htmlというファイルを作ります。

index.htmlの中身は次のようにします:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>TODOリスト</title>
    <style>
        html {
            background-color: rgb(240, 240, 240);
        }
    </style>
</head>
<body>
    <h1>TODO List</h1>
    <div>
        <ul id="todo-container"></ul>
    </div>

    <script>
        // APIからJSONを取得する
        fetch('./api/v1/list')
            .then((response) => response.json())
            .then((todoList) => {
                // id="todo-container"要素を取得する
                const todoContainer = document.querySelector('#todo-container');

                // コンテナの中身を全部消す
                todoContainer.innerHTML = '';

                // JSONの各要素に対して
                for(const item of todoList) {
                    const li = document.createElement('li');          // リスト要素
                    const label = document.createElement('label');    // ラベル
                    const checkbox = document.createElement('input'); // チェックボックス
                    checkbox.type = 'checkbox';
                    checkbox.checked = item.done;                     // 項目がdoneならチェック
                    const text = new Text(item.title);                // 項目名

                    // ラベルにチェックボックスとテキストを追加する
                    label.appendChild(checkbox);
                    label.appendChild(text);

                    // リスト要素に先ほどのラベルを追加する
                    li.appendChild(label);

                    // TODOリストにリスト要素を追加する
                    todoContainer.appendChild(li);
                }
            })
    </script>
</body>
</html>

これはFetch APIを使ってWeb APIからJSONを取得し、ブラウザ上に描画するプログラムです。Fetch APIはデフォルトではGETメソッドでのアクセスになるので、今回はURL指定だけで使えます。Fetch APIについて詳しくは「JavaScriptのFetch APIを利用してリクエストを送信する」をご覧ください。

これでサーバを起動して、「http://localhost:3000/」にアクセスします。

データが描画されました!

本当にAPIから受け取ったJSONデータを使っているのかを確認したい場合は、ブラウザ上でF12キーを押して開発者ツールを起動し、「Network」タブを開きます。そしてそのままリロードすれば通信状況が表示されるので、「list」をクリックして、「Response」をクリックすれば通信データを見ることができます。

ついでに「Headers」を見ると、GETメソッドで通信しているのも確認できます。

TODOアイテムの追加(04_add)

いままではindex.jsにベタ書きしたJSONデータを返しているだけでした。今度はAPIを通してTODOを編集できるようにしましょう!

まずは今後必要になってくるモジュールをインストールしましょう。ブラウザからのデータを解釈するmulterと、ユニークIDを生成するuuidを、npmでインストールします。

npm install --save multer uuid

次にindex.jsを編集して、項目追加用のAPIを追加します。項目の編集に対応するため、大幅に変更します。

const express = require('express'); // expressモジュールを読み込む
const multer = require('multer'); // multerモジュールを読み込む
const uuidv4 = require('uuid/v4'); // uuidモジュールを読み込む

const app = express(); // expressアプリを生成する
app.use(multer().none()); // multerでブラウザから送信されたデータを解釈する
app.use(express.static('web')); // webフォルダの中身を公開する

// TODOリストデータ
const todoList = [];

// http://localhost:3000/api/v1/list にアクセスしてきたときに
// TODOリストを返す
app.get('/api/v1/list', (req, res) => {
    // JSONを送信する
    res.json(todoList);
});

// http://localhost:3000/api/v1/add にデータを送信してきたときに
// TODOリストに項目を追加する
app.post('/api/v1/add', (req, res) => {
    // クライアントからの送信データを取得する
    const todoData = req.body;
    const todoTitle = todoData.title;
    
    // ユニークIDを生成する
    const id = uuidv4();

    // TODO項目を作る
    const todoItem = {
        id,
        title: todoTitle,
        done: false
    };

    // TODOリストに項目を追加する
    todoList.push(todoItem);

    // コンソールに出力する
    console.log('Add: ' + JSON.stringify(todoItem));

    // 追加した項目をクライアントに返す
    res.json(todoItem);
});

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

まず、読み込むモジュールが増えました。multerを使って、ブラウザから送信された内容を解釈できるようにしています。multer().none()は、ファイルのアップロードなしでテキストデータのみの解釈をします。

todoListはグローバルな位置に置きました。list APIからも、add APIからもアクセスするからです。ファイルに保存等はしていないので、サーバを再起動すれば綺麗さっぱり消えるようになっています。

次にlist APIを単にtodoListを返すだけにしました。todoListは変動するので、それに合わせるためです。

そして/api/v1/addを追加しました。このとき、app.getではなくapp.postを使っています。これは通信にGETメソッドではなくPOSTというメソッドを使うことを示しています。POSTメソッドはその名の通り、クライアントからサーバへ情報を送信するときに使用します。

/api/v1/addでは、クライアント(ブラウザ)から送られてきたデータを取り出し、todoListに追加しています。ついでに今後のために、ユニークなIDを生成して追加しています。

次はindex.htmlを編集します:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>TODOリスト</title>
    <style>
        html {
            background-color: rgb(240, 240, 240);
        }
    </style>
</head>
<body>
    <h1>TODO List</h1>
    <div>
        <ul id="todo-container"></ul>
        <input id="new-todo-item-title"><button id="new-todo-item-add-button">Add</button>
    </div>

    <script>
        // TODOリストを描画する関数
        function renderTodoList(todoList) {
            // id="todo-container"要素を取得する
            const todoContainer = document.querySelector('#todo-container');

            // コンテナの中身を全部消す
            todoContainer.innerHTML = '';

            // JSONの各要素に対して
            for(const item of todoList) {
                const li = document.createElement('li');          // リスト要素
                const label = document.createElement('label');    // ラベル
                const checkbox = document.createElement('input'); // チェックボックス
                checkbox.type = 'checkbox';
                checkbox.checked = item.done;                     // 項目がdoneならチェック
                const text = new Text(item.title);                // 項目名

                // ラベルにチェックボックスとテキストを追加する
                label.appendChild(checkbox);
                label.appendChild(text);

                // リスト要素に先ほどのラベルを追加する
                li.appendChild(label);

                // TODOリストにリスト要素を追加する
                todoContainer.appendChild(li);
            }
        }

        // APIからTODOリストを取得して描画する関数
        async function fetchTodoList() {
            // APIからJSONを取得する
            return fetch('./api/v1/list')
                .then((response) => response.json())
                .then((todoList) => {
                    renderTodoList(todoList);
                })
        }

        // APIに新しいTODOアイテムをPOSTする関数
        async function postNewTodoItem(todoItem) {
            // 送信データ'title'にタイトルテキストを追加する
            const body = new FormData();
            body.append('title', todoItem.title);

            // Fetch APIを使って、Web APIにPOSTでデータを送信する
            return fetch('./api/v1/add', {
                method: 'POST', // POSTメソッドで送信する,
                body
            }).then((response) => response.json());
        }

        const newTodoItemTitleInput = document.querySelector('#new-todo-item-title');
        const newTodoAddButton = document.querySelector('#new-todo-item-add-button');

        // Addボタンをクリックしたときに新しいTODO項目をPOSTする
        newTodoAddButton.addEventListener('click', (event) => {
            const title = newTodoItemTitleInput.value;

            // タイトルが空でなければ
            if(title) {
                // 項目をPOSTしたあとにリストを更新する
                postNewTodoItem({title}).then((item) => fetchTodoList());
            }
        });

        // 初回データ読み込み
        fetchTodoList();
    </script>
</body>
</html>

TODO項目を追加するための、Addボタンを追加しました。テキストを入力してAddボタンをクリックすると、APIに対してデータをPOSTします。

GET以外のメソッドを使って通信するときは、fetch関数の第二引数で明示的に指定する必要があります。また、POSTするデータはbodyプロパティで指定します。bodyの実体はFormDataです。

Addボタンを押してデータを送信した後、返事が返ってきたらリストを取得し、再描画するようにしています。これで常にリストが最新に保たれます。

サーバを「node index.js」で起動した後、「http://localhost:3000/」にアクセスしてみましょう。

最初は何も表示されませんが、テキストボックスに値を入力し、Addボタンを押すと新しい項目が追加されます。

Web APIを通してサーバ側のデータを操作して、クライアント側に反映する、ということが実現できました。

F12キーで開発者ツールのNetworkを開いたままAddボタンを押すと、POSTで通信できていることがわかります。

削除ボタンの追加(05_delete)

次は項目の削除ボタンを追加してみましょう。データの削除には、GETかPOSTかDELETEを使用します。どれを使用してもいいのですが、DELETEメソッドのほうがそれっぽいので、これを採用しましょう。

まずはindex.jsをいじってAPIを追加します:

const express = require('express'); // expressモジュールを読み込む
const multer = require('multer'); // multerモジュールを読み込む
const uuidv4 = require('uuid/v4'); // uuidモジュールを読み込む

const app = express(); // expressアプリを生成する
app.use(multer().none()); // multerでブラウザから送信されたデータを解釈する
app.use(express.static('web')); // webフォルダの中身を公開する

// TODOリストデータ
const todoList = [];

// http://localhost:3000/api/v1/list にアクセスしてきたときに
// TODOリストを返す
app.get('/api/v1/list', (req, res) => {
    // JSONを送信する
    res.json(todoList);
});

// http://localhost:3000/api/v1/add にデータを送信してきたときに
// TODOリストに項目を追加する
app.post('/api/v1/add', (req, res) => {
    // クライアントからの送信データを取得する
    const todoData = req.body;
    const todoTitle = todoData.title;

    // ユニークIDを生成する
    const id = uuidv4();

    // TODO項目を作る
    const todoItem = {
        id,
        title: todoTitle,
        done: false
    };

    // TODOリストに項目を追加する
    todoList.push(todoItem);

    // コンソールに出力する
    console.log('Add: ' + JSON.stringify(todoItem));

    // 追加した項目をクライアントに返す
    res.json(todoItem);
});

// http://localhost:3000/api/v1/item/:id にDELETEで送信してきたときに
// 項目を削除する。:idの部分にはIDが入る
// 例えば
// http://localhost:3000/api/v1/item/cc7cf63c-ccaf-4401-a611-f19daec0f74e
// にDELETEメソッドでアクセスすると、idがcc7cf63c-ccaf-4401-a611-f19daec0f74eのものが削除される
app.delete('/api/v1/item/:id', (req, res) => {
    // URLの:idと同じIDを持つ項目を検索
    const index = todoList.findIndex((item) => item.id === req.params.id);

    // 項目が見つかった場合
    if(index >= 0) {
        const deleted = todoList.splice(index, 1); // indexの位置にある項目を削除
        console.log('Delete: ' + JSON.stringify(deleted[0]));
    }

    // ステータスコード200:OKを送信
    res.sendStatus(200);
});

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

DELETEメソッドのときはapp.deleteを使います。そしてExpressではURLに「:xxx」と入れることで、URLの一部をxxxという名前のパラメータとして扱うことができます。今回は末尾に「:id」を入れ、項目のIDを受け取るようにしています。このIDはAddボタン追加時に作ったユニークIDです。

削除APIはとくに返す値がないので、とりあえずステータスコード200だけを返しています。ステータスコード200は「OK」の意味で、処理がうまくいったことを示します。ステータスコードを送るにはsendStatusを使います。

そしてHTML側に削除ボタンを追加します:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>TODOリスト</title>
    <style>
        html {
            background-color: rgb(240, 240, 240);
        }
    </style>
</head>
<body>
    <h1>TODO List</h1>
    <div>
        <ul id="todo-container"></ul>
        <input id="new-todo-item-title"><button id="new-todo-item-add-button">Add</button>
    </div>

    <script>
        // Deleteボタンを押したときに呼ばれる関数
        function deleteButtonListener(event) {
            const button = event.currentTarget;
            const id = button.dataset.id;

            // DELETEメソッドでAPIにアクセス
            // 削除後にリスト更新
            fetch(`./api/v1/item/${id}`, { method: 'DELETE' })
                .then(() => fetchTodoList());
        }

        // TODOリストを描画する関数
        function renderTodoList(todoList) {
            // id="todo-container"要素を取得する
            const todoContainer = document.querySelector('#todo-container');

            // コンテナの中身を全部消す
            const deleteButtonList = todoContainer.querySelectorAll('.delete-button');
            deleteButtonList.forEach((button) => button.removeEventListener('click', deleteButtonListener));
            todoContainer.innerHTML = '';

            // JSONの各要素に対して
            for(const item of todoList) {
                const li = document.createElement('li');               // リスト要素
                const label = document.createElement('label');         // ラベル
                const checkbox = document.createElement('input');      // チェックボックス
                checkbox.type = 'checkbox';
                checkbox.checked = item.done;                          // 項目がdoneならチェック
                const text = new Text(item.title);                     // 項目名
                const deleteButton = document.createElement('button'); // 削除ボタン
                deleteButton.innerText = 'Delete';
                deleteButton.classList.add('delete-button');
                deleteButton.dataset.id = item.id;
                deleteButton.addEventListener('click', deleteButtonListener);

                // ラベルにチェックボックスとテキストと削除ボタンを追加する
                label.appendChild(checkbox);
                label.appendChild(text);
                label.appendChild(deleteButton);

                // リスト要素に先ほどのラベルを追加する
                li.appendChild(label);

                // TODOリストにリスト要素を追加する
                todoContainer.appendChild(li);
            }
        }

        // APIからTODOリストを取得して描画する関数
        async function fetchTodoList() {
            // APIからJSONを取得する
            return fetch('./api/v1/list')
                .then((response) => response.json())
                .then((todoList) => {
                    renderTodoList(todoList);
                })
        }

        // APIに新しいTODOアイテムをPOSTする関数
        async function postNewTodoItem(todoItem) {
            // 送信データ'title'にタイトルテキストを追加する
            const body = new FormData();
            body.append('title', todoItem.title);

            // Fetch APIを使って、Web APIにPOSTでデータを送信する
            return fetch('./api/v1/add', {
                method: 'POST', // POSTメソッドで送信する,
                body
            }).then((response) => response.json());
        }

        const newTodoItemTitleInput = document.querySelector('#new-todo-item-title');
        const newTodoAddButton = document.querySelector('#new-todo-item-add-button');

        // Addボタンをクリックしたときに新しいTODO項目をPOSTする
        newTodoAddButton.addEventListener('click', (event) => {
            const title = newTodoItemTitleInput.value;

            // タイトルが空でなければ
            if(title) {
                // 項目をPOSTしたあとにリストを更新する
                postNewTodoItem({title}).then((item) => fetchTodoList());
            }
        });

        // 初回データ読み込み
        fetchTodoList();
    </script>
</body>
</html>

DELETEメソッドではPOSTメソッドのときと同様、fetch時にはメソッドを明示的に指定する必要があります。項目のIDは、Deleteボタン要素のユーザ定義領域(dataset)に書き込んで、それをクリック時に読み込んでいます。

これでサーバを立ち上げてアクセスしてみます。

削除ボタンでデータ削除APIにアクセスし、データを消せるようになりました!

開発者ツールのNetworkタブで見てみると、DELETEメソッドで通信し、ステータスコード200が返ってきていることがわかります。

チェックボックスの反映(06_edit)

次に編集を反映させるAPIを作りましょう。既に存在する項目の編集には、POSTメソッドかPUTメソッドを使います。PUTメソッドの方がおそらく適切なので、こちらを使います。

まずサーバ側のAPIを追加します:

const express = require('express'); // expressモジュールを読み込む
const multer = require('multer'); // multerモジュールを読み込む
const uuidv4 = require('uuid/v4'); // uuidモジュールを読み込む

const app = express(); // expressアプリを生成する
app.use(multer().none()); // multerでブラウザから送信されたデータを解釈する
app.use(express.static('web')); // webフォルダの中身を公開する

// TODOリストデータ
const todoList = [];

// http://localhost:3000/api/v1/list にアクセスしてきたときに
// TODOリストを返す
app.get('/api/v1/list', (req, res) => {
    // JSONを送信する
    res.json(todoList);
});

// http://localhost:3000/api/v1/add にデータを送信してきたときに
// TODOリストに項目を追加する
app.post('/api/v1/add', (req, res) => {
    // クライアントからの送信データを取得する
    const todoData = req.body;
    const todoTitle = todoData.title;

    // ユニークIDを生成する
    const id = uuidv4();

    // TODO項目を作る
    const todoItem = {
        id,
        title: todoTitle,
        done: false
    };

    // TODOリストに項目を追加する
    todoList.push(todoItem);

    // コンソールに出力する
    console.log('Add: ' + JSON.stringify(todoItem));

    // 追加した項目をクライアントに返す
    res.json(todoItem);
});

// http://localhost:3000/api/v1/item/:id にDELETEで送信してきたときに
// 項目を削除する。:idの部分にはIDが入る
// 例えば
// http://localhost:3000/api/v1/item/cc7cf63c-ccaf-4401-a611-f19daec0f74e
// にDELETEメソッドでアクセスすると、idがcc7cf63c-ccaf-4401-a611-f19daec0f74eのものが削除される
app.delete('/api/v1/item/:id', (req, res) => {
    // URLの:idと同じIDを持つ項目を検索
    const index = todoList.findIndex((item) => item.id === req.params.id);

    // 項目が見つかった場合
    if(index >= 0) {
        const deleted = todoList.splice(index, 1); // indexの位置にある項目を削除
        console.log('Delete: ' + JSON.stringify(deleted[0]));
    }

    // ステータスコード200:OKを送信
    res.sendStatus(200);
});

// DELETEとほぼ同じ
app.put('/api/v1/item/:id', (req, res) => {
    // URLの:idと同じIDを持つ項目を検索
    const index = todoList.findIndex((item) => item.id === req.params.id);

    // 項目が見つかった場合
    if(index >= 0) {
        const item = todoList[index];
        if(req.body.done) {
            item.done = req.body.done === 'true';
        }
        console.log('Edit: ' + JSON.stringify(item));
    }

    // ステータスコード200:OKを送信
    res.sendStatus(200);
});

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

処理はDELETEとほぼ同じで、URLも同じです。URLが同じなので、メソッドがDELETEかPUTかによって処理が分かれます。

PUTメソッドで値が送られてきたら、その値を反映する処理を書きました。今回はチェックボックスの値(done)だけを反映しています。

あとはクライアント側でこのAPIにアクセスするだけです:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>TODOリスト</title>
    <style>
        html {
            background-color: rgb(240, 240, 240);
        }
    </style>
</head>
<body>
    <h1>TODO List</h1>
    <div>
        <ul id="todo-container"></ul>
        <input id="new-todo-item-title"><button id="new-todo-item-add-button">Add</button>
    </div>

    <script>
        // チェックボックスの値が変わったときに呼ばれる関数
        function checkboxListener(event) {
            const checkbox = event.currentTarget;
            const id = checkbox.dataset.id;

            const body = new FormData();
            body.append('done', checkbox.checked.toString());

            // DELETEメソッドでAPIにアクセス
            // 削除後にリスト更新
            fetch(`./api/v1/item/${id}`, { method: 'PUT', body })
                .then(() => fetchTodoList());
        }

        // Deleteボタンを押したときに呼ばれる関数
        function deleteButtonListener(event) {
            const button = event.currentTarget;
            const id = button.dataset.id;

            // DELETEメソッドでAPIにアクセス
            // 削除後にリスト更新
            fetch(`./api/v1/item/${id}`, { method: 'DELETE' })
                .then(() => fetchTodoList());
        }

        // TODOリストを描画する関数
        function renderTodoList(todoList) {
            // id="todo-container"要素を取得する
            const todoContainer = document.querySelector('#todo-container');

            // コンテナの中身を全部消す
            const deleteButtonList = todoContainer.querySelectorAll('.delete-button');
            deleteButtonList.forEach((button) => button.removeEventListener('click', deleteButtonListener));
            const checkboxList = todoContainer.querySelectorAll('.checkbox');
            checkboxList.forEach((checkbox) => checkbox.removeEventListener('change', checkboxListener));

            todoContainer.innerHTML = '';

            // JSONの各要素に対して
            for(const item of todoList) {
                const li = document.createElement('li');               // リスト要素
                const label = document.createElement('label');         // ラベル
                const checkbox = document.createElement('input');      // チェックボックス
                checkbox.classList.add('checkbox');
                checkbox.type = 'checkbox';
                checkbox.checked = item.done;                          // 項目がdoneならチェック
                checkbox.dataset.id = item.id;
                checkbox.addEventListener('change', checkboxListener);
                const text = new Text(item.title);                     // 項目名
                const deleteButton = document.createElement('button'); // 削除ボタン
                deleteButton.innerText = 'Delete';
                deleteButton.classList.add('delete-button');
                deleteButton.dataset.id = item.id;
                deleteButton.addEventListener('click', deleteButtonListener);

                // ラベルにチェックボックスとテキストと削除ボタンを追加する
                label.appendChild(checkbox);
                label.appendChild(text);
                label.appendChild(deleteButton);

                // リスト要素に先ほどのラベルを追加する
                li.appendChild(label);

                // TODOリストにリスト要素を追加する
                todoContainer.appendChild(li);
            }
        }

        // APIからTODOリストを取得して描画する関数
        async function fetchTodoList() {
            // APIからJSONを取得する
            return fetch('./api/v1/list')
                .then((response) => response.json())
                .then((todoList) => {
                    renderTodoList(todoList);
                })
        }

        // APIに新しいTODOアイテムをPOSTする関数
        async function postNewTodoItem(todoItem) {
            // 送信データ'title'にタイトルテキストを追加する
            const body = new FormData();
            body.append('title', todoItem.title);

            // Fetch APIを使って、Web APIにPOSTでデータを送信する
            return fetch('./api/v1/add', {
                method: 'POST', // POSTメソッドで送信する,
                body
            }).then((response) => response.json());
        }

        const newTodoItemTitleInput = document.querySelector('#new-todo-item-title');
        const newTodoAddButton = document.querySelector('#new-todo-item-add-button');

        // Addボタンをクリックしたときに新しいTODO項目をPOSTする
        newTodoAddButton.addEventListener('click', (event) => {
            const title = newTodoItemTitleInput.value;

            // タイトルが空でなければ
            if(title) {
                // 項目をPOSTしたあとにリストを更新する
                postNewTodoItem({title}).then((item) => fetchTodoList());
            }
        });

        // 初回データ読み込み
        fetchTodoList();
    </script>
</body>
</html>

チェックボックスの値が変化したときにPUTで送信するようにしました。これでチェックボックスの変化がサーバ側にも反映されるようになりました。

ブラウザでチェックを入れた後、リロードしてみてください。チェックボックスの状態が反映されたままになっていると思います。

開発者ツールで見ると、PUTメソッドで通信していることがわかります。

メソッドの使い分け

GET、POST、PUT、DELETEと、様々なメソッドが登場しました。さらっと流してしまいましたが、これらはどう使い分ければいいのでしょう?

これらのメソッドは、一般的には次のように使います:

  • GET – データの取得
  • POST – データの送信
  • PUT – データの更新
  • DELETE – データの削除

4つも覚えるのが面倒だ、という場合はGETとPOSTだけで案外なんとかなる場合もあります。ですが、できれば適切に使い分ける方がいいでしょう。

さいごに

現代のウェブサービスにおいては、APIは必須とも言える重要性を持ちます。簡単なAPIを作る技術を持っていれば、いつか役にたつでしょう。

今回は、簡単なAPIのみを持つTODOリストサービスを作成しました。かなり簡易的なものだったので、実用には程遠いものですが、ここから発展させていくとでより高度なサービスを作ることもできます。

たとえばTODOデータの永続化です。今回はメモリ上にデータを持っているのでサーバを落とせば消えてしまいますが、ファイルに書き出すとか、データベースを利用するなどといったことをすれば、サーバを落としてもデータは消えません。

ユーザ認証をつけるのもいいでしょう。今回は自分だけが利用する一人用のサービスになってしまいましたが、ユーザ認証をつけてユーザごとに振り分ければ、複数のユーザから利用できるようになります。

APIを強化してもいいかもしれません。たとえば今回はチェックボックスの状態しか反映できませんでしたが、テキストの内容を変更できるようにするなど、いろいろと考えられます。

また、興味のある方は「REST」や「GraphQL」などについて調べてみてもいいかもしれません。APIの設計にも様々な手法があり、それぞれにいろんな思想や特徴があります。それらを勉強してみるのも面白いでしょう。

長い記事になりましたが、最後まで読んでいただき、ありがとうございます。