メインコンテンツへスキップ
見出し画像

🔰データベースプロフェッショナル養成工房💫第8講 TypeScriptから登録処理を呼び出そう――画面と保存部品の接続

    【プロローグ】

    ミカ「入力検証に成功しました。これで顧客を保存できますね」
    教授「まだ顧客データはブラウザーの中にいる」
    ミカ「登録ボタンも押しました」
    教授「受付で申込書を確認しただけだ。金庫番には届いていない」
    ミカ「ブラウザーからNode.jsへ渡す通路が必要なのですね」
    教授「今回はその通路と、登録処理の入口を作る」
    今回は、ブラウザー側のTypeScriptからNode.js側の登録処理を呼び出します。画面で検証したCustomerInputをJSONに変換し、POST /api/customersへ送信します。
    Node.js側ではデータを受け取り、もう一度入力を検証して、登録処理部品を呼び出します。
    今回はPostgreSQLへの保存直前までを作ります。INSERTによる本当の保存は第9講で実装します。

    【今回の到達点】

    今回完成させる流れは次のとおりです。
    1.利用者が顧客登録画面へ入力する
    2.ブラウザー側TypeScriptが入力を検証する
    3.fetchでNode.jsへJSONを送る
    4.ExpressがPOST /api/customersを受け取る
    5.Node.js側でも入力を検証する
    6.registerCustomer関数を呼び出す
    7.処理結果をJSONでブラウザーへ返す
    8.画面へ成功又はエラーを表示する
    PostgreSQLへのINSERTはまだ行わないため、画面には次のメッセージを表示します。

    Node.jsの登録処理まで到達しました。
    PostgreSQLへの保存はまだ行っていません。

    【今回追加する部品】

    新しく作る主なファイルは次のとおりです。
    ・src/validation/customer-validation.ts――共通入力検証部品
    ・src/server/customer-registration.ts――登録処理部品
    ・src/server/server.ts――画面と登録処理をつなぐAPI
    ・tsconfig.server.json――Node.js側TypeScriptの確認設定
    既存の次のファイルも修正します。
    ・src/browser/customer-form.ts――fetchによる送信を追加
    ・tsconfig.browser.json――共通検証部品を変換対象へ追加
    ・public/styles.css――送信中のボタン表示を追加

    【完成後のフォルダー構成】

    今回の作業後は、次の構成になります。

    customer-management
    ├─ database
    │  ├─ docs
    │  └─ sql
    ├─ public
    │  ├─ js
    │  │  ├─ browser
    │  │  │  └─ customer-form.js
    │  │  ├─ types
    │  │  │  └─ customer.js
    │  │  └─ validation
    │  │     └─ customer-validation.js
    │  ├─ index.html
    │  └─ styles.css
    ├─ src
    │  ├─ browser
    │  │  └─ customer-form.ts
    │  ├─ server
    │  │  ├─ customer-registration.ts
    │  │  └─ server.ts
    │  ├─ types
    │  │  └─ customer.ts
    │  ├─ validation
    │  │  └─ customer-validation.ts
    │  └─ main.ts
    ├─ dist
    ├─ package.json
    ├─ package-lock.json
    ├─ tsconfig.json
    ├─ tsconfig.browser.json
    └─ tsconfig.server.json

    public/jsの中はTypeScriptから自動生成されます。手動で編集するのはsrc内のTypeScriptです。

    【画面とNode.jsを同じサーバーから動かす】

    第7講では、画面表示用としてhttp-serverを使用しました。
    今回はExpressが次の二つを担当します。
    ・publicフォルダーのHTML、CSS、JavaScriptをブラウザーへ渡す
    ・POST /api/customersで顧客データを受け取る
    画面とAPIを同じhttp://127.0.0.1:3000から動かすため、今回はCORSの追加設定は必要ありません。

    【Node.jsのバージョンを確認する】

    Visual Studio Codeでターミナルを開き、次のコマンドを実行します。

    node --version

    たとえば、次のように表示されます。

    v22.18.0

    Express 5はNode.js 18以上を必要とします。Express公式:5.x API Reference
    Node.js 18以上であれば、そのまま進めます。

    【必要なパッケージをインストールする】

    ターミナルの現在位置がcustomer-managementであることを確認し、次のコマンドを実行します。

    npm install express

    続いて、開発用パッケージをインストールします。

    npm install --save-dev @types/express @types/node tsx

    各パッケージの役割は次のとおりです。
    ・express――Web画面とAPIを提供する
    ・@types/express――Expressの型情報
    ・@types/node――Node.jsの型情報
    ・tsx――開発中にTypeScriptをNode.jsで直接実行する
    インストール後、package.jsonとpackage-lock.jsonが更新されます。これは正常です。

    【入力検証を共通部品へ移す】

    第7講では、入力検証関数をcustomer-form.tsの中へ書きました。
    しかし、Node.js側でも同じ規則を使います。同じ検証コードを二か所へコピーすると、一方だけ修正される危険があります。
    そこで、画面側とNode.js側の両方から使える共通部品へ移します。

    【validationフォルダーを作る】

    Visual Studio Codeで次の操作を行います。
    1.エクスプローラーでsrcを右クリックします。
    2.「新しいフォルダー」を選択します。
    3.フォルダー名をvalidationと入力します。
    4.作成したvalidationを右クリックします。
    5.「新しいファイル」を選択します。
    6.ファイル名をcustomer-validation.tsと入力します。
    作成場所は次のとおりです。

    customer-management/src/validation/customer-validation.ts

    【共通入力検証部品を書く】

    customer-validation.tsへ、次のコードを入力します。

    import type {
      CustomerInput,
    } from "../types/customer.js";
    
    export type CustomerField =
      keyof CustomerInput;
    
    export type ValidationError = {
      field: CustomerField;
      message: string;
    };
    
    export function toNullableString(
      value: string,
    ): string | null {
      const trimmedValue = value.trim();
    
      return trimmedValue === ""
        ? null
        : trimmedValue;
    }
    
    export function parseCustomerInput(
      value: unknown,
    ): CustomerInput | null {
      if (
        typeof value !== "object" ||
        value === null
      ) {
        return null;
      }
    
      const data =
        value as Record<string, unknown>;
    
      const customerCode =
        data["customerCode"];
    
      const customerName =
        data["customerName"];
    
      const phone = data["phone"];
      const email = data["email"];
    
      if (
        typeof customerCode !== "string" ||
        typeof customerName !== "string"
      ) {
        return null;
      }
    
      if (
        phone !== undefined &&
        phone !== null &&
        typeof phone !== "string"
      ) {
        return null;
      }
    
      if (
        email !== undefined &&
        email !== null &&
        typeof email !== "string"
      ) {
        return null;
      }
    
      return {
        customerCode:
          customerCode.trim(),
    
        customerName:
          customerName.trim(),
    
        phone:
          typeof phone === "string"
            ? toNullableString(phone)
            : null,
    
        email:
          typeof email === "string"
            ? toNullableString(email)
            : null,
      };
    }
    
    export function validateCustomerInput(
      input: CustomerInput,
    ): ValidationError[] {
      const errors: ValidationError[] = [];
    
      if (input.customerCode === "") {
        errors.push({
          field: "customerCode",
          message:
            "顧客コードを入力してください。",
        });
      } else if (
        input.customerCode.length > 20
      ) {
        errors.push({
          field: "customerCode",
          message:
            "顧客コードは20文字以内で" +
            "入力してください。",
        });
      }
    
      if (input.customerName === "") {
        errors.push({
          field: "customerName",
          message:
            "顧客名を入力してください。",
        });
      } else if (
        input.customerName.length > 100
      ) {
        errors.push({
          field: "customerName",
          message:
            "顧客名は100文字以内で" +
            "入力してください。",
        });
      }
    
      if (
        input.phone !== null &&
        input.phone.length > 30
      ) {
        errors.push({
          field: "phone",
          message:
            "電話番号は30文字以内で" +
            "入力してください。",
        });
      }
    
      if (input.email !== null) {
        if (input.email.length > 254) {
          errors.push({
            field: "email",
            message:
              "メールアドレスは254文字以内で" +
              "入力してください。",
          });
        } else if (
          !isValidEmail(input.email)
        ) {
          errors.push({
            field: "email",
            message:
              "メールアドレスの形式を" +
              "確認してください。",
          });
        }
      }
    
      return errors;
    }
    
    function isValidEmail(
      email: string,
    ): boolean {
      const emailPattern =
        /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
      return emailPattern.test(email);
    }

    入力後、Ctrl + Sで保存します。

    【parseCustomerInputが必要な理由】

    ブラウザーから届くJSONを、最初から正しいCustomerInputだと信用してはいけません。
    たとえば、外部から次のようなデータが送られる可能性があります。

    {
      "customerCode": 123,
      "customerName": true
    }

    parseCustomerInputは、受け取った値をいったんunknownとして扱い、次の内容を確認します。
    ・全体がオブジェクトか
    ・顧客コードが文字列か
    ・顧客名が文字列か
    ・電話番号が文字列、null又は未指定か
    ・メールアドレスが文字列、null又は未指定か
    データの形が正しくなければnullを返します。

    【tsconfig.browser.jsonを修正する】

    tsconfig.browser.jsonを開き、includeへ検証フォルダーを追加します。

    修正前は次の状態です。

    "include": [
      "src/browser/**/*.ts",
      "src/types/**/*.ts"
    ]

    次のように修正します。

    "include": [
      "src/browser/**/*.ts",
      "src/types/**/*.ts",
      "src/validation/**/*.ts"
    ]

    これで、共通入力検証部品もブラウザー用JavaScriptへ変換されます。

    【serverフォルダーを作る】

    次に、Node.js側の部品を作ります。
    1.エクスプローラーでsrcを右クリックします。
    2.「新しいフォルダー」を選択します。
    3.フォルダー名をserverと入力します。
    4.作成したserverを右クリックします。
    5.「新しいファイル」を選択します。
    6.ファイル名をcustomer-registration.tsと入力します。\

    【登録処理部品を作る】

    src/server/customer-registration.tsへ、次のコードを入力します。

    import type {
      CustomerInput,
    } from "../types/customer.js";
    
    import {
      validateCustomerInput,
      type ValidationError,
    } from "../validation/customer-validation.js";
    
    export type RegistrationResult =
      | {
          success: true;
          saved: false;
          message: string;
        }
      | {
          success: false;
          saved: false;
          message: string;
          errors: ValidationError[];
        };
    
    export async function registerCustomer(
      input: CustomerInput,
    ): Promise<RegistrationResult> {
      const errors =
        validateCustomerInput(input);
    
      if (errors.length > 0) {
        return {
          success: false,
          saved: false,
          message:
            "入力内容を確認してください。",
          errors,
        };
      }
    
      return {
        success: true,
        saved: false,
        message:
          "Node.jsの登録処理まで到達しました。" +
          "PostgreSQLへの保存は" +
          "まだ行っていません。",
      };
    }

    入力後、Ctrl + Sで保存します。

    【登録処理部品の役割】

    registerCustomerは、画面やExpressの詳しい仕組みを知りません。
    受け取るものはCustomerInputです。

    input: CustomerInput

    返すものは、登録処理の結果です。

    Promise<RegistrationResult>

    今回は入力を再検証し、成功結果を返すところまでです。
    次回、この関数の成功部分からPostgreSQLの保存部品を呼び出します。
    最初から画面イベントの中へSQLを書かないことが重要です。

    【server.tsを作る】

    src/serverを右クリックして、もう一つファイルを作ります。

    server.ts

    作成場所は次のとおりです。

    customer-management/src/server/server.ts

    【Expressサーバーを書く】

    server.tsへ、次のコードを入力します。

    import { join } from "node:path";
    
    import express, {
      type Request,
      type Response,
    } from "express";
    
    import {
      parseCustomerInput,
    } from "../validation/customer-validation.js";
    
    import {
      registerCustomer,
    } from "./customer-registration.js";
    
    const app = express();
    const port = 3000;
    const host = "127.0.0.1";
    
    const publicDirectory =
      join(process.cwd(), "public");
    
    app.use(express.json());
    
    app.use(
      express.static(publicDirectory),
    );
    
    app.post(
      "/api/customers",
      async (
        request: Request,
        response: Response,
      ) => {
        console.log(
          "POST /api/customersを受信しました。",
        );
    
        const customerInput =
          parseCustomerInput(request.body);
    
        if (customerInput === null) {
          response.status(400).json({
            success: false,
            saved: false,
            message:
              "入力データの形が正しくありません。",
          });
    
          return;
        }
    
        const result =
          await registerCustomer(customerInput);
    
        if (!result.success) {
          response
            .status(400)
            .json(result);
    
          return;
        }
    
        response
          .status(200)
          .json(result);
      },
    );
    
    app.listen(
      port,
      host,
      () => {
        console.log(
          `顧客管理サーバーを起動しました。`,
        );
    
        console.log(
          `http://${host}:${port}`,
        );
      },
    );

    入力後、Ctrl + Sで保存します。

    【Expressが担当する二つの仕事】

    【画面をブラウザーへ渡す】

    次の処理が、publicフォルダーを公開します。

    app.use(
      express.static(publicDirectory),
    );

    Expressのexpress.staticを使うと、HTML、CSS、JavaScriptなどの静的ファイルを配信できます。Express公式:Serving static files

    【顧客登録APIを用意する】

    次の部分が、顧客登録APIです。

    app.post(
      "/api/customers",
      async (
        request: Request,
        response: Response,
      ) => {

    Expressのルートは、HTTPメソッド、パス、処理関数を組み合わせて定義します。Express公式:Basic routing
    今回は次の組合せです。

    HTTPメソッド:POST
    パス:/api/customers
    処理:顧客データを受け取って登録処理を呼び出す

    【JSONを受け取る】

    次の処理が、送信されたJSONを読み取れるようにします。

    app.use(express.json());

    この指定がないと、request.bodyから送信データを取得できません。

    【tsconfig.server.jsonを作る】

    プロジェクトの一番上に、Node.js側の型を確認する設定ファイルを作ります。
    1.customer-managementを右クリックします。
    2.「新しいファイル」を選択します。
    3.ファイル名をtsconfig.server.jsonと入力します。
    4.次の内容を入力します。

    {
      "extends": "./tsconfig.json",
      "compilerOptions": {
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "types": [
          "node"
        ],
        "esModuleInterop": true,
        "verbatimModuleSyntax": false,
        "declaration": false,
        "declarationMap": false,
        "noEmit": true
      },
      "include": [
        "src/server/**/*.ts",
        "src/types/**/*.ts",
        "src/validation/**/*.ts"
      ]
    }

    入力後、Ctrl + Sで保存します。
    今回はNode.js側をtsxで実行するため、noEmitをtrueにしています。

    "noEmit": true

    この設定ではJavaScriptファイルを作らず、型の誤りだけを確認します。

    【customer-form.tsを置き換える】

    src/browser/customer-form.tsを開きます。
    第7講で入力した内容をすべて選択し、次のコードへ置き換えます。

    import type {
      CustomerInput,
    } from "../types/customer.js";
    
    import {
      toNullableString,
      validateCustomerInput,
      type ValidationError,
    } from "../validation/customer-validation.js";
    
    type RegistrationResponse = {
      success: boolean;
      saved: boolean;
      message: string;
      errors?: ValidationError[];
    };
    
    const form =
      document.querySelector<HTMLFormElement>(
        "#customer-form",
      );
    
    const submitButton =
      document.querySelector<HTMLButtonElement>(
        "#register-button",
      );
    
    const messageElement =
      document.querySelector<HTMLParagraphElement>(
        "#form-message",
      );
    
    const customerCodeInput =
      document.querySelector<HTMLInputElement>(
        "#customer-code",
      );
    
    const customerNameInput =
      document.querySelector<HTMLInputElement>(
        "#customer-name",
      );
    
    const phoneInput =
      document.querySelector<HTMLInputElement>(
        "#phone",
      );
    
    const emailInput =
      document.querySelector<HTMLInputElement>(
        "#email",
      );
    
    if (
      !form ||
      !submitButton ||
      !messageElement ||
      !customerCodeInput ||
      !customerNameInput ||
      !phoneInput ||
      !emailInput
    ) {
      throw new Error(
        "顧客登録画面の部品を取得できませんでした。",
      );
    }
    
    const fieldElements:
      Record<
        keyof CustomerInput,
        HTMLInputElement
      > = {
        customerCode: customerCodeInput,
        customerName: customerNameInput,
        phone: phoneInput,
        email: emailInput,
      };
    
    form.addEventListener(
      "submit",
      async (event) => {
        event.preventDefault();
    
        clearValidationDisplay();
    
        const customerInput =
          readCustomerInput();
    
        const errors =
          validateCustomerInput(customerInput);
    
        if (errors.length > 0) {
          showValidationErrors(errors);
          return;
        }
    
        setSubmitting(true);
    
        try {
          const result =
            await sendCustomerInput(
              customerInput,
            );
    
          if (!result.success) {
            if (
              result.errors &&
              result.errors.length > 0
            ) {
              showValidationErrors(
                result.errors,
              );
            } else {
              showMessage(
                result.message,
                "error",
              );
            }
    
            return;
          }
    
          showMessage(
            result.message,
            "success",
          );
        } catch (error) {
          const errorMessage =
            error instanceof Error
              ? error.message
              : "不明なエラーです。";
    
          showMessage(
            "Node.jsの登録処理を" +
            "呼び出せませんでした。" +
            errorMessage,
            "error",
          );
        } finally {
          setSubmitting(false);
        }
      },
    );
    
    form.addEventListener(
      "reset",
      () => {
        window.requestAnimationFrame(
          () => clearValidationDisplay(),
        );
      },
    );
    
    for (
      const inputElement
      of Object.values(fieldElements)
    ) {
      inputElement.addEventListener(
        "input",
        () => clearValidationDisplay(),
      );
    }
    
    function readCustomerInput():
      CustomerInput {
      return {
        customerCode:
          customerCodeInput.value.trim(),
    
        customerName:
          customerNameInput.value.trim(),
    
        phone:
          toNullableString(phoneInput.value),
    
        email:
          toNullableString(emailInput.value),
      };
    }
    
    async function sendCustomerInput(
      input: CustomerInput,
    ): Promise<RegistrationResponse> {
      const response = await fetch(
        "/api/customers",
        {
          method: "POST",
          headers: {
            "Content-Type":
              "application/json",
          },
          body: JSON.stringify(input),
        },
      );
    
      const result =
        (await response.json())
          as RegistrationResponse;
    
      if (!response.ok) {
        return {
          ...result,
          success: false,
        };
      }
    
      return result;
    }
    
    function setSubmitting(
      isSubmitting: boolean,
    ): void {
      submitButton.disabled =
        isSubmitting;
    
      submitButton.textContent =
        isSubmitting
          ? "送信中..."
          : "顧客を登録する";
    }
    
    function clearValidationDisplay():
      void {
      messageElement.textContent = "";
    
      messageElement.classList.remove(
        "is-error",
        "is-success",
      );
    
      for (
        const inputElement
        of Object.values(fieldElements)
      ) {
        inputElement.classList.remove(
          "is-invalid",
        );
    
        inputElement.removeAttribute(
          "aria-invalid",
        );
      }
    }
    
    function showValidationErrors(
      errors: ValidationError[],
    ): void {
      messageElement.textContent =
        errors
          .map(
            (error) => `・${error.message}`,
          )
          .join("\n");
    
      messageElement.classList.add(
        "is-error",
      );
    
      for (const error of errors) {
        const inputElement =
          fieldElements[error.field];
    
        inputElement.classList.add(
          "is-invalid",
        );
    
        inputElement.setAttribute(
          "aria-invalid",
          "true",
        );
      }
    
      const firstError = errors[0];
    
      if (firstError) {
        fieldElements[
          firstError.field
        ].focus();
      }
    }
    
    function showMessage(
      message: string,
      state: "error" | "success",
    ): void {
      messageElement.textContent =
        message;
    
      messageElement.classList.add(
        state === "error"
          ? "is-error"
          : "is-success",
      );
    }

    入力後、Ctrl + Sで保存します。

    【fetchでNode.jsへ送信する】

    次の部分が、ブラウザーからNode.jsへ顧客情報を送っています。

    const response = await fetch(
      "/api/customers",
      {
        method: "POST",
        headers: {
          "Content-Type":
            "application/json",
        },
        body: JSON.stringify(input),
      },
    );

    それぞれの意味は次のとおりです。
    ・/api/customers――送信先
    ・POST――新しいデータを渡す方法
    ・Content-Type: application/json――送信内容がJSONであることを伝える
    ・JSON.stringify(input)――TypeScriptのオブジェクトをJSON文字列へ変換する
    fetchはHTTPリクエストを送り、結果をPromiseとして返します。HTTPエラーでも必ずしも例外にはならないため、response.okも確認します。MDN:Using the Fetch API

    【画面側とNode.js側で二度検証する】

    処理の流れは次のようになります。
    1.画面側のvalidateCustomerInputで検証する
    2.問題がなければNode.jsへ送る
    3.Node.js側のparseCustomerInputでデータの形を確認する
    4.registerCustomerで再び内容を検証する
    5.問題がなければ成功結果を返す
    画面側の検証は利用者へ早く知らせるためにあります。Node.js側の検証は、受信したデータを信用しないためにあります。
    同じ検証関数を共通部品にしたため、画面とNode.jsで規則が食い違いにくくなります。

    【送信中はボタンを無効にする】

    次の処理により、送信中に登録ボタンを何度も押せないようにしています。

    function setSubmitting(
      isSubmitting: boolean,
    ): void {
      submitButton.disabled =
        isSubmitting;
    
      submitButton.textContent =
        isSubmitting
          ? "送信中..."
          : "顧客を登録する";
    }

    送信開始時は次のように呼び出します。

    setSubmitting(true);

    処理が終わると、finallyで元に戻します。

    setSubmitting(false);

    【styles.cssへ送信中の表示を追加する】

    public/styles.cssを開き、.primary-button:hoverの後へ次のCSSを追加します。

    .primary-button:disabled {
      cursor: wait;
      opacity: 0.65;
      transform: none;
    }

    入力後、Ctrl + Sで保存します。

    【第7講のhttp-serverを停止する】

    第7講のhttp-serverが動いている場合は、そのターミナルをクリックしてCtrl + Cを押します。
    3000番ポートでhttp-serverとExpressを同時に起動することはできません。
    本講以降は、Expressが画面とAPIの両方を提供します。

    【Node.js側の型を確認する】

    新しいターミナルを開き、次のコマンドを実行します。

    npx tsc -p tsconfig.server.json

    エラーが表示されず、入力待ちへ戻れば成功です。
    noEmitを指定しているため、JavaScriptファイルは作成されません。ここでは型の誤りだけを確認しています。

    【ターミナル1でブラウザー用TypeScriptを監視する】

    一つ目のターミナルで、次のコマンドを実行します。

    npx tsc -p tsconfig.browser.json --watch

    次の表示を確認します。

    Found 0 errors. Watching for file changes.

    このターミナルは動かしたままにします。

    【ターミナル2でExpressを起動する】

    ターミナル右上の+を押し、二つ目のターミナルを開きます。
    次のコマンドを実行します。

    npx tsx watch src/server/server.ts

    tsx watchは、TypeScriptファイルを実行し、修正時にNode.jsの処理を再起動します。tsx公式:GitHub
    次の表示が出れば成功です。

    顧客管理サーバーを起動しました。
    http://127.0.0.1:3000

    ブラウザーで次のURLを開きます。

    http://127.0.0.1:3000

    【画面から登録処理を呼び出す】

    次の架空データを入力します。

    顧客コード:CUST-0002
    顧客名:みどり工房
    電話番号:06-0000-0000
    メールアドレス:info@midori.example

    「顧客を登録する」を押します。
    ボタンが一時的に次の表示へ変わります。

    送信中...

    処理に成功すると、画面へ次のメッセージが表示されます。

    Node.jsの登録処理まで到達しました。
    PostgreSQLへの保存はまだ行っていません。

    Expressを動かしているターミナルには、次の表示が追加されます。

    POST /api/customersを受信しました。

    これで、ブラウザーからNode.jsまで顧客データが届いたことを確認できます。

    【ブラウザーの開発者ツールで確認する】

    ブラウザーでF12キーを押し、開発者ツールを開きます。
    1.「ネットワーク」又は「Network」を選択します。
    2.顧客登録画面で登録ボタンを押します。
    3.一覧に表示されたcustomersを選択します。
    4.「Payload」又は「ペイロード」を開きます。
    送信内容として、次のようなJSONを確認できます。

    {
      "customerCode": "CUST-0002",
      "customerName": "みどり工房",
      "phone": "06-0000-0000",
      "email": "info@midori.example"
    }

    「Response」又は「応答」には、次のような結果が表示されます。

    {
      "success": true,
      "saved": false,
      "message": "Node.jsの登録処理まで到達しました。PostgreSQLへの保存はまだ行っていません。"
    }

    特に確認するのは、次の部分です。

    "saved": false

    APIへの送信は成功していますが、PostgreSQLへの保存はまだ行っていないことを表しています。

    【なぜ画面からPostgreSQLへ直接つながないのか】

    ブラウザーからPostgreSQLへ直接接続すると、次の情報をブラウザーへ置かなければなりません。
    ・データベースの接続先
    ・データベース名
    ・利用者名
    ・パスワード
    これらは利用者から見える場所へ置いてはいけません。
    今回の構成では、ブラウザーはNode.jsのAPIだけを呼び出します。Node.jsが入力検証、登録処理、データベース操作を担当します。

    【画面からSQLを見えなくする】

    画面側のTypeScriptが知っているのは、次の送信先だけです。

    POST /api/customers

    画面側は、次のことを知りません。
    ・PostgreSQLの接続方法
    ・customersテーブルの物理的な保存場所
    ・実行するINSERT文
    ・データベースの利用者名とパスワード
    画面を変更してもSQLへ影響せず、SQLを変更しても画面へ影響しにくい構成を目指します。

    【まだ保存済みとは表示しない】

    今回はAPIから成功結果が返っても、画面に「顧客を登録しました」と表示してはいけません。
    PostgreSQLへの保存が完了していないからです。
    今回のメッセージは、次の表現にしています。

    Node.jsの登録処理まで到達しました。
    PostgreSQLへの保存はまだ行っていません。

    実際に完了していない処理を、画面だけ成功したように見せないことが重要です。

    【演習】

    次の架空データを画面から送信してください。

    顧客コード:CUST-0801
    顧客名:星空サービス
    電話番号:空欄
    メールアドレス:contact@hoshizora.example

    次の三点を確認してください。
    1.画面に「Node.jsの登録処理まで到達しました」と表示される。
    2.開発者ツールのNetworkにPOST /api/customersが表示される。
    3.DBeaverで確認してもCUST-0801はまだ保存されていない。

    【演習の解答】

    DBeaverでcustomer_managementへ接続し、SQLエディターで次のSQLを実行します。

    SELECT
        customer_id,
        customer_code,
        customer_name,
        phone,
        email,
        created_at
    FROM public.customers
    WHERE customer_code = 'CUST-0801';

    結果が0件であれば正常です。
    画面からNode.jsの登録処理までは到達していますが、INSERTをまだ実行していないため、PostgreSQLには保存されていません。
    今回の処理結果は次のとおりです。

    ブラウザーでの入力    成功
    画面側の入力検証     成功
    Node.jsへのJSON送信    成功
    Node.js側の入力検証    成功
    登録処理関数の呼出し   成功
    PostgreSQLへの保存    未実装

    【うまく動かない場合】

    【EADDRINUSEと表示される】

    3000番ポートを別のプログラムが使用しています。
    第7講のhttp-serverが動いている場合は、該当するターミナルでCtrl + Cを押します。

    【Cannot find package expressと表示される】

    プロジェクト直下で次のコマンドを実行します。

    npm install express

    【Cannot find name processと表示される】

    Node.jsの型情報を確認します。

    npm install --save-dev @types/node

    その後、tsconfig.server.jsonに次の指定があるか確認します。

    "types": [
      "node"
    ]

    【画面は開くが登録ボタンでエラーになる】

    Expressを動かしているターミナルを確認します。
    次のコマンドが実行中である必要があります。

    npx tsx watch src/server/server.ts

    【404 Not Foundと表示される】

    ブラウザーの送信先とExpressのパスが一致しているか確認します。

    fetch("/api/customers", ...)
    app.post("/api/customers", ...)

    両方が同じ/api/customersでなければなりません。

    【Failed to fetchと表示される】

    次の点を確認します。
    ・Expressが起動している
    ・ブラウザーをhttp://127.0.0.1:3000で開いている
    ・file:///から開いていない
    ・ターミナルにエラーが出ていない

    【修正したコードが反映されない】

    次の順序で確認します。
    1.TypeScriptファイルをCtrl + Sで保存する。
    2.ブラウザー用ターミナルにFound 0 errorsと表示されることを確認する。
    3.Express側が再起動したことを確認する。
    4.ブラウザーでCtrl + F5を押す。

    【開発を終了する】

    作業を終了するときは、二つのターミナルを停止します。
    1.ブラウザー用TypeScriptを監視しているターミナルを選択する。
    2.Ctrl + Cを押す。
    3.Expressを実行しているターミナルを選択する。
    4.Ctrl + Cを押す。\

    【今回の完成判定】

    次の状態になれば、第8講は完了です。
    ・Expressをインストールできた
    ・共通入力検証部品を作成できた
    ・ブラウザー側とNode.js側で同じ検証関数を使用できた
    ・POST /api/customersを作成できた
    ・ブラウザーからJSONを送信できた
    ・Node.js側でJSONを受信できた
    ・登録処理関数を呼び出せた
    ・APIの結果を画面へ表示できた
    ・送信中に登録ボタンを無効化できた
    ・開発者ツールで送信内容と応答を確認できた
    ・PostgreSQLにはまだ保存されていないことを確認できた
    ・画面へデータベースの接続情報を書かずに済んだ

    【次回予告】

    ミカ「顧客データがNode.jsの登録処理まで届きました」
    教授「受付から金庫室の前までは来た」
    ミカ「次はいよいよPostgreSQLへ保存します」
    教授「そこで初めて『登録しました』と言える」
    次回は、**第9講「顧客情報をPostgreSQLへ保存しよう――INSERT処理」**です。pgを使ってPostgreSQLへ接続し、パラメーター化SQLで顧客情報を安全に保存します。


    あなたへのおすすめ