第6章 WEBアプリ開発(バックエンドAPIアプリ)

6-1. HTTPリクエストを検査する curl コマンド
6-2. DB接続クレデンシャル
6-3. バックエンドAPIアプリの設計
6-4. connectDatabase()関数
6-5. closeDatabase(db)関数
6-6. app.get( ‘/get’ ) データ読み込み
6-7. APIとポート番号のプロキシ設定
6-8. バックエンドAPIアプリの永続化
6-9. app.get( ‘/create’ ) データ書き込み
6-10. app.get( ‘/delete’ ) データ削除
6-11. app.get( ‘/update’ ) データ更新
6-12. まとめ

フロントWEBアプリとバックエンドAPIアプリをHTTPメソッドで通信します。

6-1. HTTPリクエストを検査する curl コマンド

HTTPリクエスト:GET, PUT, POST, DELETE のコマンドでの呼び出しを参考に、バックエンドAPI機能をテストしながら開発します。ポート番号は自分の環境に合わせること。

$ curl -X GET http://127.0.0.1:3002/get
$ curl -X DELETE http://127.0.0.1:3002/delete/1
$ curl -X PUT http://127.0.0.1:3002/update/1 -H "Content-Type: application/x-www-form-urlencoded"  -d "flag=111&plan=2222&result=33333"
$ curl -X POST http://127.0.0.1:3002/create -H "Content-Type: application/x-www-form-urlencoded" -d "flag=aaaa&plan=bbbbb&result=cccc"

6-2. DB接続クレデンシャル

コードの中にユーザー名やパスワードを記載すると、ログインするためのクレデンシャルが漏洩するリスクがあります。これを防ぐために、コードと環境変数のファイルを分離して記述します。
Node.jsアプリを配置するディレクトリに環境ファイル[.env]を作成、もしくはDBに接続するための環境変数を追加します。

DB_HOST=DBサーバーのドメイン名(もしくはIPアドレス)
DB_USER=データベースに接続するユーザー名
DB_PASS=データベースに接続するユーザーのパスワード
DB_NAME=使うデータベース名

6-3. バックエンドAPIアプリの設計

URIアクセスを待ち受けし、データベースのレコードを読み書きした結果を返す関数を設計します。
ディレクトリ[~/app]に、新しいファイル[app-api.js]を作成します。

// ----------------------------
// 初期設定:モジュール読み込み。変数定義。
// ----------------------------
const express = require("express");                 // Expressモジュール読み込み
const app = express();                              // アプリサーバーとしてインスタンス化
const mysql = require('mysql');                     // MySQLモジュール読み込み
const cors = require('cors');                       // CORS(Cross-Origin Resource Sharing)モジュールを読み込み
const port = 3003;                                  // TCPポート番号を指定
require('dotenv').config();                         //環境変数[~/app/.env]を process.env に代入

// ----------------------------
// `.bashrc` で設定した環境変数からユーザー情報を取得
// ----------------------------

// ----------------------------
// listen()メソッドを実行して3000番台ポートで待ち受け。
// ----------------------------
var server = app.listen(port, function(){
    console.log("Node.js待ち受けポート:" + server.address().port);
});

// ----------------------------
// WEBブラウザは、異なるオリジン(ドメイン)へのアクセスは基本的にブロックする。
// CORS()関数は、リモートサーバーからのPOST, GET, PUT, DELETEメソッドのアクセスを許可する。詳細な制御も可能。
// ----------------------------
//app.use(cors({
//    origin: 'https://example.com',
//    methods: ['GET', 'POST', 'PUT', 'DELETE'],
//    allowedHeaders: ['Content-Type', 'Authorization']
//  }));
//app.use(cors());

// ----------------------------
// フォームデータを解析
// ----------------------------
app.use(express.urlencoded({ extended: true })); // フォーム形式(name=value)
app.use(express.json());                         // JSON形式({ "key": "value" })


// ------------------------------------
// データベースに接続する関数
// ------------------------------------
function connectDatabase() {}

// ------------------------------------
// データベースを閉じる関数
// ------------------------------------
function closeDatabase(db) {}

// ------------------------------------
// データ読み込み
// ------------------------------------
app.get('/get', (req, res) => {});

// ------------------------------------
// データ書き込み
// ------------------------------------
app.post('/create', (req, res) => {});

// ------------------------------------
// データ削除
// ------------------------------------
app.delete('/delete/:id', (req, res) => {});

// ------------------------------------
// データ更新
// ------------------------------------
app.put('/update/:id', (req, res) => {});

// ------------------------------------
// アプリサーバーが応答することを検査
// ------------------------------------
app.get('/', (req, res) => {
    res.send('いえーい');
    console.log('いえーい');
});

CORSモジュールをインストールします(未インストールの場合、エラーが表示されます:Error: Cannot find module ‘cors’)

$ npm install cors

アプリをテストします。

$ node app-api.js
Node.js待ち受けポート:3003

待ち受けポート番号が表示されたら成功です。

ブラウザからアクセスできること

http://ドメイン名:3003/

同じホストの別のシェルからアクセスできること

$ curl -X GET http://localhost:3003/

6-4. connectDatabase()関数

データベースに接続するコードを記述します。データベースを閉じる関数とペアで呼び出します。

function connectDatabase() {
  // MySQLデータベースの接続情報を設定
  const db = mysql.createConnection({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME
  });
  // データベースに接続
  db.connect((err) => {
      if (err) throw err;
      console.log('MySQL接続は成功');
  });
  return db;
}

6-5. closeDatabase(db)関数

接続しているデータベース db を指定して閉じます。

function closeDatabase(db) {
  // データベース接続を閉じる
  db.end((err) => {
    if (err) throw err;
    console.log('MySQL接続を閉じました');
  });
}

6-6. app.get( ‘/get’ ) データ読み込み

CURDにおけるデータ操作の基本形は 1. DB接続 2. クエリ実行 3. DB閉じる。
読み出し[SELECT * FROM テーブル]の場合:

app.get('/get', (req, res) => {
  // 1.データベースに接続
  const db = connectDatabase(); 
  // 2.データを読み出すクエリ
  let sql = 'SELECT * FROM todo';
  db.query(sql, (err, results) => {
      if (err) throw err;
      res.json(results);
      // 3.データベースを閉じる
      closeDatabase(db); 
  });
});

アプリをテストします。

$ node app-api.js
Node.js待ち受けポート:3003

待ち受けポート番号が表示されたら成功です。

ブラウザからアクセスできること

http://ドメイン名:3003/

# 読み出したレコードが表示される
[{"id":1,"flag":"0","plan":"テスト","result":"-"}]

同じホストの別のシェルからアクセスできること

$ curl -X GET http://localhost:3003/

6-7. APIとポート番号のプロキシ設定

APIアプリのURL 例えば https://servername.com/app や /login などにアクセスしたときに、ポート番号 http://localhost:3000 などに遷移させる設定

$ sudo nano /etc/nginx/sites-available/ドメイン名(例:web.hogehoge.com)

# APIアプリ用に追加
server {
    listen 80;
    server_name web.funnygeekjp.com;
    return 301 https://$host$request_uri;       # HTTPをHTTPSにリダイレクト; 404;  managed by Certbot

    location /user {                            # webコンテンツのディレクトリ
        alias /home/user/www;
        index index.html;
    }

    # アプリ用に追加(Node.js APIへのプロキシ)
    location /app {
        proxy_pass http://localhost:3000/chat;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_buffering off;
    }

    location /login {
        proxy_pass http://localhost:3000/login;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_buffering off;
    }

    listen 443 ssl; # SSサーバー証明書の管理 by Certbot
    ssl_certificate /etc/letsencrypt/live/web.hogehoge.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/web.hogehoge.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    # アプリ用にプロキシを追加
    location /api3001/ {
        proxy_pass http://localhost:3001/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
    location /api3002/ {
        proxy_pass http://localhost:3002/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

設定を変更したら nginx を再起動する

$ sudo systemctl restart nginx

ブラウザで検査する

# ポート番号でアクセス
http://ome.funnygeekjp.com:3007/

# URIでアクセス
https://ome.funnygeekjp.com/api3007/

6-8. バックエンドAPIアプリの永続化

node.js アプリの実行は、[$ node app.js]で起動しています。Linuxサーバーを再起動したときも、同じコマンドでアプリを起動しなければなりません。

これが大事なのですが、アプリがエラーで終了したときは、手動でもう一度コマンド[$ node app.j]で再起動します。
つまり、エラーが発生すると、誰かが手動でアプリを起動するまではアプリを使えないということになります。

ここで pm2(プロセスマネージャー)を使います。pm2は、エラー発生時に自動再起動したり、ログ管理したりできます。

pm2 のインストール

$ npm install -g pm2

インストールできたか検査
$ pm2 -V

pm2でアプリを起動

アプリを起動するコマンド。app.jsはアプリ名を指定します(app-api.jsとか)

$ pm2 start app.js

その他、pm2の使い方

▼実行中のプロセス一覧
$ pm2 list
▼ログを表示
$ pm2 logs
▼プロセスを停止
$ pm2 stop app.js
▼プロセスを削除
$ pm2 delete app.js
▼サーバー再起動したときにアプリを再起動する
$ pm2 start app.js
$ pm2 save
$ pm2 startup
[PM2] Init System found: systemd

6-9. app.get( ‘/create’ ) データ書き込み

読み取り[/get]と同様に、書き込み[/create]のコード:
書き込む情報は各自異なるので自分のDatabase設定に合わせること
・テーブル名
・データ項目
・書き込む値(文字列は”や””などコーテーションで囲う。数値はコーテーションなし)

app.post('/create', (req, res) => {
    // 1.データベースに接続
    const db = connectDatabase();
    //plan memo
    var inputPlan = req.body.plan;
    var inputMemo = req.body.memo;

    // 2.データを書き込むクエリ
    sql = "INSERT INTO tbl_kano (plan, result, flag) VALUES ('" + inputPlan + "', '" + inputMemo + "', 0)";

    db.query(sql, (err, results) => {
      if (err) {
        console.log('DB書き込みに失敗 X');
        throw err;
      }
      res.json(results);
      console.log('DB書き込み成功 O');
    });

    // 3.データベースを閉じる
    closeDatabase(db);
});

6-10. app.get( ‘/delete’ ) データ削除

レコードを削除するコード:
削除するテーブルのレコード番号は各自にあわせること
・テーブル名
・削除するレコードの指定条件(id=1などWHERE句)

app.delete('/delete/:id', (req, res) => {
  // 1.データベースに接続
  const db = connectDatabase();
  // 2.URLパラメータから `id` を取得(修正: `req.params.id` を使用)
  let myID = req.params.id;
  // IDが空ならエラーを返す
  if (!myID) {
    return res.status(400).json({ message: "削除するIDが指定されていません。" });
  }
  // SQLクエリを安全に実行
  let sql = "DELETE FROM todo WHERE id = ?";
  db.query(sql, [myID], (err, results) => {
    if (err) {
      console.error("DBのレコード削除に失敗:", err);
      return res.status(500).json({ message: "データベースエラー" });
    }
    res.json({ message: `ID[${myID}] を削除しました`, data: results });
    console.log(`DBのID[${myID}] を削除成功`);
  });
  // 3.データベースを閉じる
  closeDatabase(db);
});

6-11. app.get( ‘/update’ ) データ更新

レコードを書き換えするコード:
どのレコードをどのように書き換えるのか各自のデータ項目にあわせること
・レコードを書き換えする条件
・テーブル、項目名

app.put('/update/:id', (req, res) => {
  // 1.データベースに接続
  const db = connectDatabase();

  // 2.URLパラメータから `id` を取得
  let myID = req.params.id;
  let myPlan = req.body.plan;
  let myResult = req.body.result;

  // リクエスト検査
  if (!myID) {
    console.log("更新するID[%d]が不足しています。", myID);
    return res.status(400).json({ message: "更新するIDまたはデータが不足しています。" });
  }

  // SQLクエリを実行
  let sql = "UPDATE tbl_kano SET plan = ?, result = ? WHERE id = ?";
  db.query(sql, [myPlan, myResult, myID], (err, results) => {
    if (err) {
      console.error("レコードの更新に失敗:", err);
      return res.status(500).json({ message: "データベースエラー" });
    }
    res.json({ message: `ID[${myID}] のデータを更新しました`, data: results });
  });
  console.log("[9]データベース更新を終了しDB閉じます");

  // 3.データベースを閉じる
  closeDatabase(db);
});

6-12. まとめ

バックエンドAPIアプリ(BFF; Backend for Frontend)は完成した。
データベースに、4つのAPIで読み書き・更新削除ができるようになった。

動作API命名例JS関数の命名例, HTTPメソッドHTMLボタン命名例
Create(レコード追加)/getitemAdd(), POSTデータ追加
Read(レコード読み取り)/createitemRead(), GETデータ読取
Update(レコード更新)/updateitemUpdate(), PUTデータ更新
Delete(レコード削除)/deleteItemDelete(), DELETEデータ削除

次は、見た目の楽しいWEBフロントアプリを開発します

以上

コメントを残す