🐸

Nuxt3 × Prisma Todoアプリ開発

に公開

簡単なTODOアプリ開発

フロントのUIはTailwind CSSとElementPlusを使用します。
導入手順は以下参照
https://zenn.dev/tspk/articles/3c49fe9e096ed6

フロント側のUIから作成していきます。
app.vue

<script setup lang="ts">
// 入力値
const todo = ref<string>("");
// TODOを格納する配列
const todoList = ref<string[]>([]);

// TODOリストに追加
const addTodo = () => {
  if (todo.value.trim() === "") {
    return;
  }

  todoList.value.push(todo.value);
  todo.value = "";
};

// TODOリストから削除
const removeTodo = (index: number) => {
  todoList.value.splice(index, 1);
};
</script>

<template>
  <div
    class="flex flex-col items-center justify-center min-h-screen bg-gray-100 w-full"
  >
    <h1 class="text-5xl font-bold text-slate-700 mb-8">TODO App</h1>
    <div class="w-96">
      <el-row :gutter="24">
        <el-col :span="20">
          <el-input v-model="todo" placeholder="Please input" size="large" />
        </el-col>
        <el-col :span="4">
          <el-button
            type="primary"
            class="text-2xl"
            size="large"
            @click="addTodo"
          >
            追加
          </el-button>
        </el-col>
      </el-row>
      <template v-if="todoList.length > 0">
        <el-row>
          <el-col :span="12">
            <el-card class="w-[410px] mt-4">
              <div
                v-for="(item, index) in todoList"
                :key="index"
                class="text-lg flex justify-between items-center py-2"
              >
                <span>{{ item }}</span>
                <el-button type="danger" @click="removeTodo(index)"
                  >削除</el-button
                >
              </div>
            </el-card>
          </el-col>
        </el-row>
      </template>
    </div>
  </div>
</template>

よく見るTODOアプリだと思うので解説はしません。
追加と削除の機能だけ実装しています。

初期画面

TODO追加ver

Prismaを導入

npm install prisma --save-dev
npm install @prisma/client
npx prisma init

Prisma スキーマ作成
prisma/schema.prisma

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

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id        Int      @id @default(autoincrement())
  title     String
  createdAt DateTime @default(now())
}

マイグレーション & DB 作成

npx prisma migrate dev --name init

Prisma クライアント共通ファイル作成
server/utils/prisma.ts
https://www.prisma.io/docs/orm/more/help-and-troubleshooting/nextjs-help

// PrismaClient クラスを @prisma/client からインポート
import { PrismaClient } from "@prisma/client";

// TypeScript の型定義を使って、Node.js のグローバルスコープに
// prisma を保存できるように準備(再利用のため)
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

// Prisma クライアントのインスタンスを作成または再利用する
// 開発環境ではファイル変更時にモジュールが何度も再読み込みされるため、
// new PrismaClient() を何回も呼ぶと DB 接続エラーが出る(接続数オーバー)
// それを防ぐため、一度作成したクライアントをグローバルに保存して使い回す
export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    // 開発時に発行される SQL クエリをログとして出力(デバッグ用)
    log: ["query"],
  });

// 本番環境でなければ、作成した PrismaClient を globalThis に保存
// 開発中にファイルが更新されても、同じインスタンスを再利用できるようになる
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

API エンドポイント作成
server/api/todos/index.get.ts

// グローバルで使い回す Prisma クライアントをインポート
import { prisma } from "~/server/utils/prisma";

// defineEventHandler は Nuxt3 の API エンドポイントを定義する関数
// この関数が呼ばれると、非同期でTodoの一覧を返す
export default defineEventHandler(async () => {
  // Prismaを使ってtodoテーブルのすべてのレコードを取得
  // createdAt(作成日時)の降順(新しい順)で並び替え
  return await prisma.todo.findMany({
    orderBy: {
      createdAt: "desc", // 日付が新しい順に表示
    },
  });
});

server/api/todos/create.post.ts

// グローバルで使い回される Prisma クライアントをインポート
import { prisma } from "~/server/utils/prisma";

// Nuxt3 の API エンドポイントを定義(POSTリクエストなどを受け付ける)
export default defineEventHandler(async (event) => {
  // クライアントから送られてきたリクエストのボディを読み取る
  // この中に { title: "xxx" } のようなデータが含まれている前提
  const body = await readBody(event);

  // Prisma を使って Todo テーブルに新しいレコードを作成する
  return await prisma.todo.create({
    data: {
      // クライアントから受け取ったタイトルをDBに保存
      title: body.title,
    },
  });
});

server/api/todos/[id].delete.ts

// グローバルで使い回す Prisma クライアントをインポート
import { prisma } from "~/server/utils/prisma";

// Nuxt3 の API エンドポイントを定義
export default defineEventHandler(async (event) => {
  // URL パラメータ(例: /api/todos/3 の「3」)を取得し、数値に変換
  const id = Number(getRouterParam(event, "id"));

  // 数字でないIDが渡された場合、400エラーを返す(バリデーション)
  if (isNaN(id)) {
    throw createError({ statusCode: 400, statusMessage: "Invalid ID" });
  }

  // Prisma を使って、指定されたIDのTodoを削除
  return await prisma.todo.delete({
    where: { id }, // idが一致するレコードを削除
  });
});

フロント修正
app.vue

<script setup lang="ts">
// interface
interface TodoList {
  id: number;
  title: string;
}
// 入力値
const todo = ref<string>("");
// TODOを格納する配列
const todoList = ref<TodoList[]>([]);

// TODOリストに追加
const addTodo = async () => {
  try {
    if (!todo.value.trim()) return;
    await $fetch("/api/todos/create", {
      method: "POST",
      body: { title: todo.value },
    });
    todo.value = "";
    await fetchTodos();
  } catch (error) {
    console.error(error);
  }
};

// TODOリストから削除
const deleteTodo = async (id: number) => {
  try {
    await $fetch(`/api/todos/${id}`, { method: "DELETE" });
    await fetchTodos();
  } catch (error) {
    console.error(error);
  }
};

const fetchTodos = async () => {
  try {
    todoList.value = await $fetch<TodoList[]>("/api/todos");
  } catch (error) {
    console.error(error);
  }
};

onMounted(async () => {
  await fetchTodos();
});
</script>

<template>
  <div class="flex flex-col items-center min-h-screen bg-gray-100 w-full pt-24">
    <h1 class="text-5xl font-bold text-slate-700 mb-8">TODO App</h1>
    <div class="w-96">
      <el-row :gutter="24">
        <el-col :span="20">
          <el-input v-model="todo" placeholder="Please input" size="large" />
        </el-col>
        <el-col :span="4">
          <el-button
            type="primary"
            class="text-2xl"
            size="large"
            @click="addTodo"
          >
            追加
          </el-button>
        </el-col>
      </el-row>
      <template v-if="todoList.length > 0">
        <el-row>
          <el-col :span="12">
            <el-card class="w-[410px] mt-4">
              <div
                v-for="todo in todoList"
                :key="todo.id"
                class="text-lg flex justify-between items-center py-2"
              >
                <span class="ml-2">{{ todo.title }}</span>
                <el-button type="danger" @click="deleteTodo(todo.id)"
                  >削除</el-button
                >
              </div>
            </el-card>
          </el-col>
        </el-row>
      </template>
    </div>
  </div>
</template>

これで、簡易ですがTODOアプリができました。
はじめてPrismaを使ってみたので間違えていたらすみません

Discussion