1
3

【Node.js】MySQL と接続して CRUD 機能を実装する

Posted at

はじめに

この記事では、Node.js / TypeScript / MySQL を使ってユーザーの CRUD 機能を実装する手順について記載します。

開発環境

開発環境は以下の通りです。

  • Windows11
  • VSCode
  • Talend API Tester
  • Docker Engine 27.0.3
  • Docker Compose 2
  • MySQL 9.0.0
  • Node.js 20.15.1
  • npm 10.8.2
  • @types/node 20.14.11
  • ts-node 10.9.2
  • TypeScript 5.5.3

開発環境構築

以下の手順で開発環境を構築します。

実装方針

src/index.ts でルーティングを行い、src/users.ts で CRUD 処理を行います。
各 CRUD 処理は以下の流れで行います。

  1. DB接続
  2. CRUD クエリ実行
  3. DB接続切断
  4. レスポンス作成

Read

Read 機能を実装します。Read 機能は、全件取得と一件取得の2種類実装します。

全件取得

src/users.ts で全ユーザー取得処理を行います。
他の機能でも利用する型定義とエラー処理も記載します。

src/users.ts
import { ServerResponse } from "http";
import { createConnection } from "./database";
import { RowDataPacket } from "mysql2";

type User = RowDataPacket & {
  id: string;
  name: string;
  email: string;
};

const handleError = (res: ServerResponse, error: unknown) => {
  console.error("Error: ", error);
  res.writeHead(500, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ message: "Internal Server Error" }));
};

export const getUsers = async (res: ServerResponse) => {
  try {
    const connection = await createConnection();
    const [rows] = await connection.query<User[]>("SELECT * FROM users");
    await connection.end();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(rows));
  } catch (error) {
    handleError(res, error);
  }
};

src/users.ts で実装した getUserssrc/index.ts で呼び出します。HTTP リクエストメソッドの種類で処理を振り分けます。

src/index.ts
import { createServer, IncomingMessage, ServerResponse } from "http";
import { createConnection } from "./database";
import { getUsers } from "./users";

const handleRequest = async (req: IncomingMessage, res: ServerResponse) => {
  const [_, resource] = req.url?.split("/") || [];

  if (resource !== "users") {
    res.writeHead(404, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ massage: "Not Found" }));
    return;
  }

  switch (req.method) {
    case "GET":
      await getUsers(res);
      break;
    default:
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ massage: "Not Found" }));
  }
};

const server = createServer(handleRequest);

const PORT = process.env.PORT || 3000;

server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
  createConnection();
});

動作確認をします。
初期データとしてDBに登録していた2件のユーザー情報が取得できます。

image.png

一件取得

一件取得の場合は、指定された ID のユーザーの有無による処理の分岐も行います。

src/users.ts
...

export const getUserById = async (id: number, res: ServerResponse) => {
  try {
    const connection = await createConnection();
    const [rows] = await connection.query<User[]>(
      "SELECT * FROM users WHERE id = ?",
      [id]
    );
    connection.end();
    const user = rows[0];

    if (!user) {
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ message: "User not found" }));
      return;
    }

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(user));
  } catch (error) {
    handleError(res, error);
  }
};

一件取得も GET を利用するので、リクエストに ID が含まれるかどうかで呼び出す関数を選択します。

src/index.ts
...
const handleRequest = async (req: IncomingMessage, res: ServerResponse) => {
  const [_, resource, resourceId] = req.url?.split("/") || [];

  if (resource !== "users") {
    res.writeHead(404, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ massage: "Not Found" }));
    return;
  }

  const DECIMAL_RADIX = 10; // Named constant for the radix
  const id = resourceId ? parseInt(resourceId, DECIMAL_RADIX) : null;

  switch (req.method) {
    case "GET":
      id ? await getUserById(id, res) : await getUsers(res);
      break;
    default:
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ massage: "Not Found" }));
  }
};
...

動作確認をします。
ID が 1 のユーザー情報が取得できます。

image.png

存在しないユーザー ID を指定すると、404 エラーになります。

image.png

Create

{
  "name": string;
  "email": string;
}

リクエストボディから "name"email を取り出す関数を追加します。この関数を使って取得した "name"email をユーザーを作成するクエリ実行時に引数として渡します。

src/users.ts
...
const parseBody = (req: IncomingMessage): Promise<Omit<User, "id">> => {
  return new Promise((resolve, reject) => {
    let body = "";
    req.on("data", (chunk) => {
      // Convert the chunk to a string and append it to the body
      body += chunk.toString();
    });
    req.on("end", () => {
      try {
        // Parse the accumulated body as JSON
        const parseBody: Omit<User, "id"> = JSON.parse(body);
        resolve(parseBody);
      } catch (error) {
        // Reject the promise if parsing fails
        reject(error);
      }
    });
  });
};

...

export const createUser = async (req: IncomingMessage, res: ServerResponse) => {
  try {
    const user: Omit<User, "id"> = await parseBody(req);
    const connection = await createConnection();
    await connection.query("INSERT INTO users (name, email) VALUES (?, ?)", [
      user.name,
      user.email,
    ]);
    connection.end();
    res.writeHead(201, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ message: "User created" }));
  } catch (error) {
    handleError(res, error);
  }
};

HTTP リクエストメソッドが POST の時、createUser を実行します。

src/index.ts
...
const handleRequest = async (req: IncomingMessage, res: ServerResponse) => {
  ...
  switch (req.method) {
    case "GET":
      id ? await getUserById(id, res) : await getUsers(res);
      break;
    case "POST":
      await createUser(req, res);
      break;
    default:
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ massage: "Not Found" }));
  }
};
...

動作確認をします。
成功すると、201"User created" が返ってきます。

image.png

また、全ユーザー取得をすると、先ほど作成したユーザーも含まれていることが確認できます。

image.png

Update

{
  "name"?: string;
  "email"?: string;
}

更新クエリを実行する前に更新対象ユーザーが DB に存在するか確認します。存在しない場合、404 を返します。
存在する場合、Create 時と同じくリクエストボディから "name"email を取り出し、クエリ実行時に引数として渡します。

src/users.ts
...
export const updateUser = async (
  id: number,
  req: IncomingMessage,
  res: ServerResponse
) => {
  try {
    const connection = await createConnection();

    const [targetUsers] = await connection.query<User[]>(
      "SELECT * FROM users WHERE id = ?",
      [id]
    );
    const targetUser = targetUsers[0];

    if (!targetUser) {
      connection.end();
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ message: "User not found" }));
      return;
    }

    const user: Omit<User, "id"> = await parseBody(req);
    await connection.query(
      "UPDATE users SET name = ?, email = ? WHERE id = ?",
      [user.name, user.email, id]
    );
    connection.end();

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ message: "User updated" }));
  } catch (error) {
    handleError(res, error);
  }
};

HTTP リクエストメソッドが POST の時、updateUser を実行します。

src/index.ts
const handleRequest = async (req: IncomingMessage, res: ServerResponse) => {
  ...
  switch (req.method) {
    case "GET":
      id ? await getUserById(id, res) : await getUsers(res);
      break;
    case "POST":
      await createUser(req, res);
      break;
    case "PUT":
      id && (await updateUser(id, req, res));
      break;
    default:
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ massage: "Not Found" }));
  }
};

動作確認をします。
成功すると、200"User updated" が返ってきます。

image.png

対象ユーザーの情報を取得すると、"name" と "email" が更新されていることが確認できます。

image.png

なお、存在しないユーザーを更新しようとすると、404 エラーになります。

image.png

Delete

updateUser と同じくクエリを実行する前に削除対象ユーザーが DB に存在するか確認します。存在しない場合、404 を返します。
存在する場合、クエリを実行します。

src/index.ts
export const deleteUser = async (id: number, res: ServerResponse) => {
  try {
    const connection = await createConnection();

    const [targetUsers] = await connection.query<User[]>(
      "SELECT * FROM users WHERE id = ?",
      [id]
    );
    const targetUser = targetUsers[0];

    if (!targetUser) {
      connection.end();
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ message: "User not found" }));
      return;
    }

    await connection.query("DELETE FROM users WHERE id = ?", [id]);
    connection.end();

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ message: "User deleted" }));
  } catch (error) {
    handleError(res, error);
  }
};

HTTP リクエストメソッドが DELETE の時、deleteUser を実行します。

src/index.ts
const handleRequest = async (req: IncomingMessage, res: ServerResponse) => {
  ...
  switch (req.method) {
    case "GET":
      id ? await getUserById(id, res) : await getUsers(res);
      break;
    case "POST":
      await createUser(req, res);
      break;
    case "PUT":
      id && (await updateUser(id, req, res));
      break;
    case "DELETE":
      id && (await deleteUser(id, res));
      break;
    default:
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ massage: "Not Found" }));
  }
};
...

動作確認をします。
成功すると、200"User deleted" が返ってきます。

image.png

削除したユーザーの情報を取得しようとすると、404 エラーになります。

image.png

なお、存在しないユーザーを削除しようとした場合も 404 エラーになります。

image.png

参考

関連

1
3
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
1
3