📚

Next.jsでPrismaとSupabaseを使ってみる API編

2023/10/10に公開

概要

前回はTODOリストでPrismaの設定までおこなったので、今度はNext.jsでAPIのエンドポイントを作成して、TODOの追加・削除・アップデートなどやってみる。

https://zenn.dev/kiriyama/articles/89bac9034bbe7a

APIでエンドポイントを作成

appディレクトリ配下にapi/todoというフォルダ構成にしてroute.tsを作成する。
Next.js 13からはAPIのファイルはroute.tsという名前で作成する必要があります。

またTODOを削除するにはIDが必要になるので、[id]フォルダを作成して、route.tsで削除など行う。下記がディレクトリ構成。

app/
├── api/todo
│       └── route.ts
│       └── [id] 
│           └── route.ts
|── page.tsx

Prisma Clientでインスタンスを作成

まず、データベースの操作するにはprismaのインスタンスを作成する必要がある。
前回、Prisma Clientをインストールしたのでこれを使用する。

app/api/todo/route.ts
import { PrismaClient } from "@prisma/client";

//インスタンスを作成
const prisma = new PrismaClient();

// データベースに接続する関数
export const connect = async () => {
    try {
        //prismaでデータベースに接続
        prisma.$connect();
    } catch (error) {
        return Error("DB接続失敗しました")
    }
}

上記のように、PrismaClientをインポートしてnew PrismaClient()でインスタンスを作成。
接続用のconnect関数を作っておいて、GETPOSTなどから呼び出せるようにしておきます。
このconnect()などは以後、説明は省略。

GETでTODO一覧を取得

TODOの一覧を取得するためのGET関数を記載します。
とりあえずSupabaseの画面から記事をダミーで作成するなどしておく。
ちなみにGET関数だが、Getなど小文字を利用すると実行されないので注意する。

app/api/todo/route.ts

// データベースからデータを取得する
export const GET = async (req: Request) => {
    try {
        await connect();
        const todos = await prisma.todo.findMany();
	
        return NextResponse.json({todos},{ status: 200 })

    } catch (error) {
        return NextResponse.json({ messeage: "Error" },{ status: 500 })

    } finally {
      //必ず実行する
        await prisma.$disconnect();
    }
}

TODOのデータを取得するにはfindMany()を実行します。
またprisma.todotodoだがスキーマを設定した際のTodoの小文字となるので、例えばスキーマの設定でPostとしたら、prisma.postとなる。

取得したデータをtodosで受け取って、NextResponse.jsonで返す。
finallyでは成功してもエラーでも接続を解除するための、$disconnect()を実行する。

また、値を返すのにres.statusとしていたが、新しいNext.jsではNextResponseを利用する。

return res.status(200).json({ message: "ユーザー登録成功" })

これでGETによるTODO一覧の取得するAPIのGETは終わり。
この後は同じようにPOSTでTODOを追加などするのだが、一旦VSCODEのPOSTMANの拡張機能を使ってAPIでGETを叩くと以下のようにJSONが取得できてるのが分かる。

POSTでTODOを追加する

今度は、TODOを追加してみる。

api/todo/route.ts

export const POST = async (req: Request, res: NextResponse) => {
    const { title } = await req.json();
    try {
        await connect();
        const todo = await prisma.todo.create({
            data: {
                title: title,
                createdAt: new Date()
            }
        });

        return NextResponse.json({ message: "投稿完了"}, { status: 200 })

    } catch (error) {
        return NextResponse.json({ messeage: "投稿失敗" }, { status: 500 })

    } finally {
        await prisma.$disconnect();
    }
}

GETと違うのフォームからの値を取得する必要がある。
以前までのNext.jsでフォームの値を取得するにはres.bodyだったが、新しいNext.jsでは、req.json()で取得となるので、分割代入でTODOのタイトルを取得している。

フロントエンド側の処理は後術している。

あとはprismaのcreate()datatitlecreatedAtとして現在の日付を渡している。

DELETEでTODOを削除する

削除の場合はIDが必要となるのでAPIは、/api/todo/[id]/route.tsに記載する。
Prisma Clientでインスタンスを作成やconnect()で接続する関数は別ファイルにしてもいいかも。

api/todo/[id]/route.ts
export const DELETE = async (req: Request, { params }: { params: Params }) => {
    try {
        const targetId:number = Number(params.id);
     
        await connect();
        const todos = await prisma.todo.delete({
            where: { id: targetId }
        });

        return NextResponse.json({ message: "削除成功", todos }, { status: 200 })

    } catch (error) {
        return NextResponse.json({ messeage: "削除失敗" }, { status: 500 })

    } finally {
        await prisma.$disconnect();
    }
}

フロントエンド側の後で説明するとして、削除するTODOのidをAPI側に送るのは以下のようになる。

//IDが2のTODOを削除したい場合のURL
http://localhost:3000/api/todo/2

このIDを取得する為に、params.idで受け取る。こちらはフォルダを[id]としてるので、[num]とかだと、params.numとなる。

また別の方法で受け取る場合は、下記のようにreq.urlでURLを取得してsplitで分割することで所得する方法もある。

api/todo/[id]/route.ts
export const DELETE = async (req: Request, res: NextResponse) => {
const id: number = Number(req.url.split("/blog/")[1]);
}

受け取ったら、 prisma.todo.delete()whereでIDを指定すればいい。
あとはNextResponse.json()でメッセージを返している。

TODO一覧を表示させる

あとはTODOの一覧を表示させてみる。
新しいNext.jsなのでサーバーコンポーネントとなっている。
一旦、分かりやすくTODOの一覧のみを記載。

page.tsx
const getTodoList = async () => {
  const res = await fetch('http://localhost:3000/api/todo')
  const json = await res.json()
  return json.todos
}

type todo = {
  id: number
  title: string
  created_at: Date
}

export default async function Home() {

  const todoList = await getTodoList()

  return (
    <main>
      <h1>Next.js + TypeScript + Prisma + supabase</h1>

      {todoList.map((todo:todo) => (
        <div key={todo.id}>
          <h2>{todo.title}</h2>
        </div>
      ))}
    </main>
  )
}

getTodoList関数でfetchを利用して記事を利用している。
あとはReactあるあるのmap()でループ処理をして一覧を表示させている。

TODOの登録と削除を追加する。

今度はTODOの登録と削除を追加。
まずは登録するために登録用のフォームと処理を追加。今回はServerActionsを利用。
一覧表示と同じファイルに記載するが、分かりすくする為に必要な箇所のみ。

TODOを登録する処理

page.tsx
export default async function Home() {

  //server actionsを利用してTODOを追加
  const addPost = async (formData: FormData) => {
    "use server"
    const text: FormDataEntryValue | null = formData.get('text')
    if (!text) return

    const res = await fetch('http://localhost:3000/api/todo', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        title: text
      })
    })
    revalidatePath('/')
  }
 return(
	<form action={addPost}>
	<input type="text" name="text" placeholder="New task..." />
	<button>Add Task</button>
	</form>
 )
}

<form>タグのactionに実行する関数addPostを指定する。
関数でuse serverの宣言をしてServerActionとして処理する。
フォームの値はFormDataget()で取得して、受け取ったテキストをfetch()でAPI側にPOSTでデータをJSON形式で送信すると追加される。

TODOを削除する処理

今回は削除もServerActionを利用する為フォームを利用している。

page.tsx
export default async function Home() {
  //server actionsを利用してTODOを削除
 const deletePost = async (formData: FormData) => {
    "use server"
    const id: FormDataEntryValue | null = formData.get('id')
    if (!id) return
    await fetch(`http://localhost:3000/api/todo/${id}`, {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        id: id
      })
    })
    revalidatePath('/')
  }
  
 return(
	<ul className="space-y-4">
            {todoList.map((todo: todo) => (
               <li key={todo.id}>
                   <span>{todo.title}</span>
                   <form>
                    <input type="hidden" name="id" value={todo.id} />
                  <button formAction={deletePost}>削除</button>
                </form>
             </li>
           ))}
       </ul>
 )
}

TODOを削除するには、TODO一覧を取得した際に、各TODOの削除ボタン部分は<form>タグで括って、IDをinput要素のvalueに指定すればいい。
非表示にするためにhiddenで非表示にする。

まとめ

一旦、これでTODOの追加・削除とTODOの一覧表示はできた。
これまでの流れを見ていただくとわかると思うが、*Supabaseを利用しているものの、Supabaseを意識した処理はほとんどなく初期設定で指定しただけである。
初期設定でMongoDBなどの設定すれば、Prismaでの処理だけで完結しそうです。

ほんとは、Supabaseにはdoneというカラムがあるので、チェックを入れた要素は取り消し線を入れるための処理を入れる予定だがまた今度。

Discussion