Masayan tech blog.

  1. ブログ記事一覧>
  2. ExpressとPrismaでサクッと作るAPI:Part3(フロントエンドの実装)

ExpressとPrismaでサクッと作るAPI:Part3(フロントエンドの実装)

公開日

目次

  • 環境
  • フロントエンドの実装
  • 動作確認

環境

フロントエンドアプリケーション

  • Next v13.4.19
  • react v18.2.0
  • TypeScript v5.2.2

API

  • Node v18.17.1
  • Express v4.18.2
  • TypeScript v5.2.2

DB

  • Prisma v5.2.0
  • mysql v8.0

ローカル環境

DockerAPIのCRUD実装

フロントエンドの実装

簡単なユーザー登録フォームを作成し、Create,Read,Update,DeleteのリクエストをAPIに送る

ディレクトリ/ファイル分割

API側同様、features/userというディレクトリをプロジェクトディレクトリの直下に作成し、細かく分けるようにする

フォームの見た目の部分

app/page.tsx

"use client";
import { useUserForm } from "@/features/user/hooks/useUserForm";

export default function Home() {
  const {
    name,
    email,
    users,
    handleChange,
    editUser,
    submit,
    findUser,
    deleteUser,
  } = useUserForm();

  return (
    <main className="flex min-h-screen flex-col items-center p-24">
      <ul>
        {users.length !== 0 &&
          users.map((user) => (
            <li key={user.id}>
              {`ID: ${user.id} | 名前: ${user.name}`}{" "}
              <span onClick={() => editUser(user)}>| ✏️編集 |</span>
              <span onClick={() => deleteUser(user)}>🗑️削除</span>
              <span onClick={() => findUser(user)}>| 詳細</span>
            </li>
          ))}
      </ul>
      <div className="max-w-md mx-auto mt-5 p-5 bg-gray-100 rounded-md">
        <div className="mb-4">
          <label htmlFor="name" className="block text-gray-700 font-bold mb-2">
            名前
          </label>
          <input
            type="text"
            id="name"
            name="name"
            value={name}
            onChange={(e) => handleChange(e, `name`)}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-green-500"
            required
          />
        </div>
        <div className="mb-4">
          <label htmlFor="email" className="block text-gray-700 font-bold mb-2">
            メールアドレス
          </label>
          <input
            type="email"
            id="email"
            name="email"
            value={email}
            onChange={(e) => handleChange(e, `email`)}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-green-500"
            required
          />
        </div>
        <div className="text-center">
          <button
            onClick={submit}
            className="bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-4 rounded-md focus:outline-none focus:shadow-outline-green"
          >
            送信
          </button>
        </div>
      </div>
    </main>
  );
}

Httpリクエストを送信するピュアなTypeScript関数

features/user/functions/client.ts

import { END_POINT } from "@/features/user/constant";
import { User } from "@/features/user/types/user";
import axios from "axios";

/**
 * ユーザーを作成する
 */
export const create = async (name: string, email: string): Promise<User> => {
  const res = await axios.post(END_POINT, {
    data: { name, email },
  });

  return res.data.user;
};

/**
 * ユーザーを取得する
 */
export const fetch = async (): Promise<User[]> => {
  const res = await axios.get(END_POINT);
  return res.data.users;
};

/**
 * 単一のユーザーを取得する
 */
export const find = async (id: string): Promise<User> => {
  const res = await axios.get(`${END_POINT}/${id}`);
  return res.data.user;
};

/**
 * ユーザーを更新する
 */
export const update = async (user: User): Promise<User> => {
  const res = await axios.patch(END_POINT, {
    data: { id: user.id, name: user.name, email: user.email },
  });

  return res.data.user;
};

/**
 * ユーザーを削除する
 */
export const destroy = async (user: User): Promise<User> => {
  const res = await axios.delete(END_POINT, {
    data: { id: user.id },
  });

  return res.data.user;
};

stateの管理が必要なロジック

features/user/hooks/useUserForm.tsx

app/page.tsxにロジックを詰め込まないようにするためにstateが必要な処理はこちらに記述。主にfeatures/user/functions/client.tsを呼び出すだけの責務

import {
  create,
  destroy,
  fetch,
  find,
  update,
} from "@/features/user/functions/client";
import { User } from "@/features/user/types/user";
import { AxiosError } from "axios";
import { useEffect, useState } from "react";

export const useUserForm = () => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [users, setUsers] = useState<User[]>([]);
  const [selectedUser, setSelectedUser] = useState<User | undefined>(undefined);

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement>,
    key: "name" | "email"
  ) => {
    if (key === "name") {
      setName(e.target.value);
      return;
    }
    setEmail(e.target.value);
  };

  const editUser = (user: User) => {
    setSelectedUser(user);
    setName(user.name);
    setEmail(user.email);
  };

  const createUser = async () => {
    try {
      const user = await create(name, email);

      setUsers((prev) => [...prev, user]);

      setName("");
      setEmail("");
    } catch (e: unknown) {
      if (e instanceof AxiosError) {
        // TODO: 画面に表示
        if (e.response?.data.statusCode === 400) {
          console.log(e.response?.data.error);
          return;
        }

        console.log("System error");
      }
    }
  };

  const updateUser = async (user: User) => {
    const updatedUser = await update({ id: user.id, name, email });

    const i = users.findIndex((u) => u.id === user.id);
    const newUsers = [...users];
    newUsers.splice(i, 1, updatedUser);

    setUsers(newUsers);

    setSelectedUser(undefined);
    setName("");
    setEmail("");
  };

  const submit = () => {
    if (selectedUser) {
      updateUser(selectedUser);
      return;
    }

    createUser();
  };

  const findUser = async (user: User) => {
    const _user = await find(user.id);
    console.log(_user);
  };

  const deleteUser = async (user: User) => {
    const deletedUser = await destroy(user);
    const i = users.findIndex((u) => u.id === deletedUser.id);
    const newUsers = [...users];
    // i番目の要素を削除する
    newUsers.splice(i, 1);

    setUsers(newUsers);
  };

  useEffect(() => {
    (async () => {
      const users = await fetch();
      setUsers(users);
    })();
  }, []);

  return {
    name,
    email,
    users,
    handleChange,
    editUser,
    submit,
    findUser,
    deleteUser,
  };
};

userに関する型定義

features/user/types/user.d.ts

export type User = {
  id: string;
  name: string;
  email: string;
};

userに関する定数類

features/user/constant.ts

export const END_POINT = "http://localhost:18000/users";

動作確認

新規作成

取得

更新

削除

Part3はここまで。Part4では主にAPIの簡単なエラーハンドリングを行う

まとめ

いかがでしたでしょうか。本記事は、ExpressとPrismaでAPIをサクッと作るシリーズのPart3です。Part3では主にフロントエンドアプリケーションをNext.jsで作成し、APIへhttpリクエストを送信しています。Part4では主にAPIの簡単なエラーハンドリングを行います