NextAuth+PrismaでCRUDを実装してみた
はじめに
株式会社サードスコープのインターン生の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として送信されることになります。