Skip to Content

CRUD操作を実装しよう

このページで学ぶこと

  • CRUD(作成・取得・更新・削除)の全操作を実装できる
  • フロントエンドとバックエンドを組み合わせた Todoアプリが作れる

CRUD とは

操作英語HTTP メソッドPrisma
作成CreatePOSTcreate
取得ReadGETfindMany / findUnique
更新UpdatePUT / PATCHupdate
削除DeleteDELETEdelete

Todoアプリ全機能を実装する

バックエンド(API Routes)

一覧取得・作成: app/api/todos/route.js

import { NextResponse } from 'next/server'
import { prisma } from '../../../lib/prisma'
 
// GET /api/todos — 全件取得
export async function GET() {
  const todos = await prisma.todo.findMany({
    orderBy: { createdAt: 'desc' }
  })
  return NextResponse.json(todos)
}
 
// POST /api/todos — 新規作成
export async function POST(request) {
  const { title } = await request.json()
 
  if (!title?.trim()) {
    return NextResponse.json({ error: 'titleは必須です' }, { status: 400 })
  }
 
  const todo = await prisma.todo.create({
    data: { title: title.trim(), userId: 1 }  // ← 今は仮のID。認証を実装したら本物のユーザーIDに差し替える
  })
  return NextResponse.json(todo, { status: 201 })
}
javascript

更新・削除: app/api/todos/[id]/route.js

import { NextResponse } from 'next/server'
import { prisma } from '../../../../lib/prisma'
 
// PATCH /api/todos/:id — 完了状態を切り替え
export async function PATCH(request, { params }) {
  const { id } = await params  // Next.js 16 では await が必要
  const { completed } = await request.json()
 
  const todo = await prisma.todo.update({
    where: { id: parseInt(id) },
    data: { completed }
  })
  return NextResponse.json(todo)
}
 
// DELETE /api/todos/:id — 削除
export async function DELETE(request, { params }) {
  const { id } = await params  // Next.js 16 では await が必要
 
  await prisma.todo.delete({ where: { id: parseInt(id) } })
  return NextResponse.json({ message: '削除しました' })
}
javascript

フロントエンド: app/page.js

'use client'
import { useState, useEffect } from 'react'
 
export default function TodoApp() {
  const [todos, setTodos] = useState([])
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(true)
 
  // 一覧取得
  useEffect(() => {
    fetch('/api/todos')
      .then((res) => res.json())
      .then((data) => {
        setTodos(data)
        setLoading(false)
      })
  }, [])
 
  // 追加
  const addTodo = async () => {
    if (!input.trim()) return
    const res = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: input }),
    })
    const newTodo = await res.json()
    setTodos([newTodo, ...todos])
    setInput('')
  }
 
  // 完了状態の切り替え
  const toggleTodo = async (id, completed) => {
    const res = await fetch(`/api/todos/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ completed: !completed }),
    })
    const updatedTodo = await res.json()
    setTodos(todos.map((t) => (t.id === id ? updatedTodo : t)))
  }
 
  // 削除
  const deleteTodo = async (id) => {
    await fetch(`/api/todos/${id}`, { method: 'DELETE' })
    setTodos(todos.filter((t) => t.id !== id))
  }
 
  if (loading) return <p>読み込み中...</p>
 
  return (
    <div style={{ padding: '32px', maxWidth: '500px' }}>
      <h1>Todoアプリ</h1>
 
      {/* 入力フォーム */}
      <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
          placeholder="新しいTodoを入力"
          style={{ flex: 1, padding: '8px' }}
        />
        <button onClick={addTodo}>追加</button>
      </div>
 
      {/* Todoリスト */}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map((todo) => (
          <li
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: '8px',
              padding: '8px 0',
              borderBottom: '1px solid #eee',
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id, todo.completed)}
            />
            <span style={{ flex: 1, textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.title}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
 
      {todos.length === 0 && <p>Todoがありません。追加してみましょう!</p>}
    </div>
  )
}
javascript

動作確認

  1. npm run dev でサーバーを起動する
  2. http://localhost:3000 にアクセスする
  3. 以下を試す
    • Todoを入力して「追加」を押す → DBに保存される
    • チェックボックスをクリックする → 完了状態が切り替わる
    • 「削除」を押す → DBから削除される
    • ページをリロードする → データが消えないことを確認する

確認しよう

  • Todo の追加・取得・更新・削除がすべて動作した
  • ページをリロードしてもデータが保存されていた(DBに永続化)
  • Supabase ダッシュボードの Table Editor でデータが増減するのを確認した

ここまでで何ができた?

おめでとうございます!ここまでで以下が揃っています:

  • データをDBに保存できる(Supabase + Prisma)
  • APIでデータを取得・作成・更新・削除できる
  • フロントエンドからAPIを呼んで、画面に反映できる

これは Webアプリの骨格 です。あとは認証を足して「誰のデータか」を管理できるようにすれば、本格的なアプリになります。


このTodoアプリをさらに発展させるには

現状のTodoアプリには1つ課題があります。userId: 1 と固定しているため、
誰がアクセスしても同じデータが見える状態です。

認証を追加すると:

  1. ユーザーごとにログイン・サインアップできる
  2. 自分のTodoだけが表示される(userId に実際のログインユーザーのIDが入る)
  3. ログアウトするとデータにアクセスできなくなる

次のページのセキュリティ(SQLインジェクション対策)を学んだ後、認証に進みます。


AIに聞いてみよう

「PrismaのupsertとcreateOrUpdateの違いを教えてください。どんな場面で使いますか?」


次のステップ

🔒 SQLインジェクションを理解して対策する

Last updated on