Third Scope LogoDevelopers Blog

NextAuth+PrismaでCRUDを実装してみた

筆者:konno

はじめに

株式会社サードスコープのインターン生のkonnoです。前回の記事「0から始めるNextAuth」に引き続き、NextAuth + PrismaでCRUD(Create・Read・Update・Delete)操作を実装していきます。

CRUDとは?

CRUDとは、作成(Create)・表示(Read)・更新(Update)・削除(Delete)という4つの基本的な機能の略称のことです。

この記事の目標

この記事を読むことで、ソーシャルログイン機能を利用し、ログインしたユーザーが記事の作成・表示・更新・削除機能を実装できるようになります。

開発環境

  • Next.js(v13.4)
  • TypeScript
  • Prisma
  • NextAuth
  • MySQL(PlanetScale)

前提

前回の記事「0から始めるNextAuth」の続きなので、NextAuthでのログイン機能とPrismaの設定が完了していることが前提となります。もしまだ実装していない場合は、こちらの記事を参考に実装を進めてください!

また、Next.jsにはAPI Routesという機能が備わっているため、そちらを利用してAPIを作成していきます!

APIRoutesとは

通常、APIを作成する際には、RailsやLaravel、NestJSなどの他のフレームワークを使用して別のプロジェクトを作成する手法が一般的です。

しかし、Next.jsでは同じプロジェクト内でAPIを作成し、通信することができるという仕組みが備わっています。

Axiosインストール

API通信するためのライブラリとしてAxiosを使用するので、installしておいてください。

$ yarn add axios

スキーマ作成

まずはスキーマを作成します。prisma/schema.prismaファイルに下記コードを記述してください。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
  relationMode = "prisma"
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
  @@index([userId])
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  posts         Post[]
}

model Post {
  id            String    @id @default(cuid())
  title         String
  content       String
  user_id       String
  user          User      @relation(fields: [user_id], references: [id])

  @@index([user_id])
}

今回は、「Userが複数のPostを持っている」ため、1対多(User:Posts)の関係で作成します。

Create(作成)

早速、記事を作成する機能を実装します。

バックエンド

pages/api/posts/index.tsを作成し、下記の記述を追加します。

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    // データの作成
    const { title, content, user_id } = req.body;
    const post = await prisma.post.create({
      data: {
        user_id,
        title,
        content,
      },
    });
    res.status(201).json(post);
  }
}

if (req.method === “POST”) ・・・localhost:3000/api/postsにPOSTメソッドでアクセスした場合に、postをcreate(作成)します。

const { title, content, user_id } = req.body・・・title、content、 user_idをrequestのbodyから取り出します。後でreq.bodyに値を送信する記述をするので、今は「何となく書けばいいんだな」という程度で大丈夫です。

prisma. post.create・・・data:{}の中に記述した値を作成してくれます。今回はreq.bodyで送られた値  を作成してもらうという記述になります。

res.status(201).json(post)・・・レスポンスとして201(成功)のステータスコードとpostをJSON形式で返しています。

フロントエンド

記事作成用のAPIを実装したので、APIに値を送信する関数を実装します(APIを叩く関数)。

pages/posts/index.tsxを作成して、下記の記述を追加します。

type User = {
  id: string;
  name?: string;
  email?: string;
  image?: string;
  posts: Post[];
};

type Post = {
  id: string;
  title: string;
  content: string;
  user_id: string;
  user: User;
};

const Posts = () => {
  const { data: session } = useSession();
  const [user, setUser] = useState<User>();
  const [titleText, setTitleText] = useState("");
  const [contentText, setContentText] = useState("");

// APIを叩く関数
 const createPost = async () => {
    try {
      if (titleText === "" || contentText === "") return;
      const response = await axios.post("/api/posts", {
        user_id: session ? session.user.id : null,
        title: titleText,
        content: contentText,
      });
      setTitleText("");
      setContentText("");
      console.log(response.data);
    } catch (error) {
      console.error(error);
    }
  };

 return (
      <div>
       <h1>Posts</h1>
       <label>Title:</label>
       <input value={titleText} onChange={(e) => setTitleText(e.target.value)} />
       <label>Text:</label>
       <textarea
         value={contentText}
         onChange={(e) => setContentText(e.target.value)}
       />
       <button onClick={createPost}>Create Post</button>
    </div>
 )
};

export default Posts;

上記コードのcreatePost関数を解説します。

const response = await axios.post("/api/posts", {
        user_id: session ? session.user.id : null,
        title: titleText,
        content: contentText,
      });

axiots.post(“第一引数”, “第二引数(オプショナル)”)

axios.post・・・API作成時にif (req.method === “POST”)と指定したので、この記述でpostメソッドを指定しています(createする際の通信はpost)。

第一引数・・・叩くapiのパスを指定しています。先ほど作成したapiはapi/postsにあるので、”api/posts”としています。

第二引数・・・ここにbodyに入れたい値を指定しています。第二引数に記述した値が、”api/posts”のreq.bodyに渡ります。user_idには現在ログインしているユーザーのid、titleとcontentにはフォーム内の値を送信しています。

以上で作成機能は完成です。現時点では、作成した値を画面上で確認することができませんので、次のセクションで表示できるようにしていきます。

もし、作成ができているか確認したい場合は、Prisma Studioを使用して確認できます。

ターミナルで、

$ npx prisma studio

と入力するとデータベースに保存された値を確認することができます。

Read(表示)

先ほど作成した記事を表示できるようにしていきます。

バックエンド

pages/api/postsに下記コードを追加してください。

if (req.method === "POST") {
    // データの作成
    const { title, content, user_id } = req.body;
    const post = await prisma.post.create({
      data: {
        user_id,
        title,
        content,
      },
    });
    res.status(201).json(post);
  } 
// ここから追加
else if (req.method === "GET") {
    const { id } = req.body;
    const user = await prisma.user.findUnique({
      where: {
        id: id,
      },
      include: {
        posts: true,
      },
    });
    res.status(200).json(user);
}

findUnique・・・指定した値に一致するデータを一つ取り出すことができます。

where・・・where: {}で指定した値が含まれたデータを取得できます。今回の例では、bodyから取り出したidと一致するuserを取得しています。

include・・・include: {}内でリレーション先のデータを指定することで、リレーション先のデータを全て取得することができます。今回はpostsとリレーションを繋いでいるので、includeに含めています。実際にフロントで表示する時には、user.postsというふうに記述して表示することができます(今回定義した型の場合)。

フロントエンド

pages/posts/index.tsxに記述を追加します。

const Posts = () => {
  const { data: session } = useSession();
  const [user, setUser] = useState<User>();
  const [titleText, setTitleText] = useState("");
  const [contentText, setContentText] = useState("");

// ログインしているユーザーが変わったら、そのユーザーのデータを取得する
useEffect(() => {
    if (session) fetchData(session?.user.id);
  }, [session]);


// ログインしているユーザー情報を取得
  const fetchData = async (id: string) => {
    try {
      const response = await axios.get(`/api/user/${id}`, { data: { id: id } });
      setUser(response.data);
    } catch (error) {
      console.error(error);
    }
  };

// APIを叩く関数
 const createPost = async () => {
    try {
      if (titleText === "" || contentText === "") return;
      const response = await axios.post("/api/posts", {
        user_id: session ? session.user.id : null,
        title: titleText,
        content: contentText,
      });
      setTitleText("");
      setContentText("");
      if (session) fetchData(session?.user.id); // 記述を追加(データを再取得)
      console.log(response.data);
    } catch (error) {
      console.error(error);
    }
  };

 return (
      <div>
       <h1>Posts</h1>
       <label>Title:</label>
       <input value={titleText} onChange={(e) => setTitleText(e.target.value)} />
       <label>Text:</label>
       <textarea
         value={contentText}
         onChange={(e) => setContentText(e.target.value)}
       />
       <button onClick={createPost}>Create Post</button>

// ここから追加
       <ul>
        {user &&
          user.posts.map((post) => (
            <li key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.content}</p>
            </li>
          ))}
      </ul>
// ここまで
    </div>
 )
};

export default Posts;

fetchData関数を解説します。

// ログインしているユーザー情報を取得
  const fetchData = async (id: string) => {
    try {
      const response = await axios.get(`/api/user/${id}`, { data: { id: id } });
      setUser(response.data);
    } catch (error) {
      console.error(error);
    }
  };

axios.get・・・情報を取得するメソッドはgetメソッドなので、getを指定します。

引数で(id: string)には、ログインしているユーザーのidを入れます。

useEffect(() => {
    if (session) fetchData(session?.user.id);
  }, [session]);

上記の記述でページにアクセスしたときに、ログインユーザーのidを入れています。

次に、UIに表示してあげます。

<ul>
        {user &&
          user.posts.map((post) => (
            <li key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.content}</p>
            </li>
          ))}
 </ul>

user.posts.mapとすることでuserに紐付いたpostを全て表示することができます。

また、createPost関数に下記の一行を追加してください。createした後に再度データを取得しないと、UIが更新されません。

 if (session) fetchData(session?.user.id); // 記述を追加(データを再取得)

Update(更新)

次は、作成した値を更新できるようにします。

バックエンド

pages/api/posts/index.tsに下記の記述を追加します。

else if (req.method === "PUT") {
    // データの更新
    const { id, title, content } = req.body;
    const post = await prisma.post.update({
      where: { id },
      data: {
        title,
        content,
      },
    });
    res.status(200).json(post);

prisma.post.update・・・updateと書くと、whereで指定した値のデータを更新することができます。

フロントエンド

pages/posts/index.tsxに下記コードを記述します。

const updatePost = async (id: string) => {
    try {
      const response = await axios.put("/api/posts", {
        id,
        title: titleText,
        content: contentText,
      });
      setTitleText("");
      setContentText("");
      console.log(response.data);
      if (session) fetchData(session?.user.id); // データを再取得
    } catch (error) {
      console.error(error);
    }
  };

await.axios.put・・・データの更新にはputメソッドを使います。

update関数も更新処理が完了した後にデータを再取得して更新内容をUIに反映させます。

<ul>
        {user &&
          user.posts.map((post) => (
            <li key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.content}</p>
             // 下記一行を追加
              <button onClick={() => updatePost(post.id)}>Update</button>
            </li>
          ))}
 </ul>

Updateボタンを押したら、updatePost関数を実行するようにします。また、引数としてpostのidを指定することを忘れないでください。

Delete(削除)

最後に削除機能を追加します。

バックエンド

page/api/posts/[id].tsファイルを作成し、下記のコードを記述してください。

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "DELETE") {
    const { id } = req.query;
    const post_id = String(id);
    await prisma.post.delete({
      where: { id: post_id },
    });
    res.status(204).end();
  }
}

req.query・・・queryの値が入っています。今回はapi/posts/[id].tsファイルなので、[id]の値が入ります。

また、削除するときはdeleteメソッドを使うので、post.deleteと記述します。

フロントエンド

pages/posts/index.tsxに下記コードを記述します。

const deletePost = async (id: string) => {
    try {
      await axios.delete(`/api/posts/${id}`);
      console.log("Post deleted");
      if (session) fetchData(session?.user.id); // データを再取得
    } catch (error) {
      console.error(error);
    }
  };

axios.deleteと書いて、methodをdeleteに指定します。また、今回はbodyに値を入れるのではなく、queryに値を入れるので、

/api/posts/${id}
と書いています。

つまり、deletePost関数の引数で渡ってきた値がそのままqueryとして送信されることになります。

0から始めるNextAuth

次の記事
chevron next
記事一覧へ戻る