Next.jsでPrismaを使ったREST API

2025-08-26

Next.jsアプリケーションでPrismaを活用したREST APIを構築する方法を、初心者向けに徹底解説します。このガイドでは、基本的なAPI設計から実践的な実装テクニックまで、3000字を超える詳細な説明を行います。

はじめに:Next.js APIルートの基本

Next.jsはAPIルート機能を内蔵しており、pages/apiディレクトリにファイルを作成するだけでサーバーサイドAPIを実装できます。Prismaと組み合わせることで、データベース連携の必要なAPIを簡単に構築できます。

基本的なAPIルートの構造

// pages/api/example.ts
import { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    // GETリクエストの処理
    res.status(200).json({ message: 'GETリクエストを受け取りました' })
  } else {
    // その他のHTTPメソッドの処理
    res.setHeader('Allow', ['GET'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

プロジェクトの準備

必要な依存関係の確認

以下のパッケージがインストールされていることを確認してください:

npm install @prisma/client next

Prismaのセットアップ

前回までの記事で設定したPrismaの構成が完了していることを前提とします。prisma/schema.prismaに適切なモデルが定義されており、lib/prisma.tsでPrismaクライアントが初期化されている状態です。

ユーザーAPIの実装

1. ユーザー取得API (GET)

pages/api/users/index.tsを作成:

import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    try {
      const users = await prisma.user.findMany({
        select: {
          id: true,
          name: true,
          email: true,
          createdAt: true,
        },
      })
      res.status(200).json(users)
    } catch (error) {
      console.error(error)
      res.status(500).json({ error: 'ユーザーの取得に失敗しました' })
    }
  } else {
    res.setHeader('Allow', ['GET'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

2. ユーザー作成API (POST)

同じファイルにPOSTメソッドの処理を追加:

// 既存のhandler関数内に追加
if (req.method === 'POST') {
  try {
    const { name, email } = req.body

    if (!name || !email) {
      return res.status(400).json({ error: '名前とメールアドレスは必須です' })
    }

    const newUser = await prisma.user.create({
      data: {
        name,
        email,
      },
    })

    res.status(201).json(newUser)
  } catch (error) {
    console.error(error)

    if (error.code === 'P2002') {
      return res.status(409).json({ error: 'このメールアドレスは既に使用されています' })
    }

    res.status(500).json({ error: 'ユーザーの作成に失敗しました' })
  }
}

3. 個別ユーザー操作API (GET, PUT, DELETE)

pages/api/users/[id].tsを作成:

import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../../lib/prisma'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const userId = parseInt(req.query.id as string)

  if (isNaN(userId)) {
    return res.status(400).json({ error: '無効なユーザーIDです' })
  }

  switch (req.method) {
    case 'GET':
      try {
        const user = await prisma.user.findUnique({
          where: { id: userId },
          select: {
            id: true,
            name: true,
            email: true,
            createdAt: true,
          },
        })

        if (!user) {
          return res.status(404).json({ error: 'ユーザーが見つかりません' })
        }

        res.status(200).json(user)
      } catch (error) {
        console.error(error)
        res.status(500).json({ error: 'ユーザーの取得に失敗しました' })
      }
      break

    case 'PUT':
      try {
        const { name, email } = req.body

        const updatedUser = await prisma.user.update({
          where: { id: userId },
          data: {
            name,
            email,
          },
        })

        res.status(200).json(updatedUser)
      } catch (error) {
        console.error(error)

        if (error.code === 'P2025') {
          return res.status(404).json({ error: 'ユーザーが見つかりません' })
        }

        res.status(500).json({ error: 'ユーザーの更新に失敗しました' })
      }
      break

    case 'DELETE':
      try {
        await prisma.user.delete({
          where: { id: userId },
        })

        res.status(204).end()
      } catch (error) {
        console.error(error)

        if (error.code === 'P2025') {
          return res.status(404).json({ error: 'ユーザーが見つかりません' })
        }

        res.status(500).json({ error: 'ユーザーの削除に失敗しました' })
      }
      break

    default:
      res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
      res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

投稿(Post)APIの実装

1. 投稿一覧取得API (GET)

pages/api/posts/index.tsを作成:

import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    try {
      const { published, authorId } = req.query

      const posts = await prisma.post.findMany({
        where: {
          published: published ? published === 'true' : undefined,
          authorId: authorId ? parseInt(authorId as string) : undefined,
        },
        include: {
          author: {
            select: {
              id: true,
              name: true,
            },
          },
        },
        orderBy: {
          createdAt: 'desc',
        },
      })

      res.status(200).json(posts)
    } catch (error) {
      console.error(error)
      res.status(500).json({ error: '投稿の取得に失敗しました' })
    }
  } else {
    res.setHeader('Allow', ['GET'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

2. 投稿作成API (POST)

同じファイルにPOST処理を追加:

// 既存のhandler関数内に追加
if (req.method === 'POST') {
  try {
    const { title, content, published = false, authorId } = req.body

    if (!title || !content || !authorId) {
      return res.status(400).json({ 
        error: 'タイトル、内容、著者IDは必須です' 
      })
    }

    const newPost = await prisma.post.create({
      data: {
        title,
        content,
        published,
        author: {
          connect: { id: parseInt(authorId) },
        },
      },
      include: {
        author: {
          select: {
            id: true,
            name: true,
          },
        },
      },
    })

    res.status(201).json(newPost)
  } catch (error) {
    console.error(error)

    if (error.code === 'P2025') {
      return res.status(404).json({ error: '著者が見つかりません' })
    }

    res.status(500).json({ error: '投稿の作成に失敗しました' })
  }
}

リレーションを活用した複雑なAPI例

ユーザーとその投稿を同時に取得

pages/api/users/[id]/posts.tsを作成:

import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../../lib/prisma'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const userId = parseInt(req.query.id as string)

  if (isNaN(userId)) {
    return res.status(400).json({ error: '無効なユーザーIDです' })
  }

  if (req.method === 'GET') {
    try {
      const userWithPosts = await prisma.user.findUnique({
        where: { id: userId },
        include: {
          posts: {
            where: {
              published: true,
            },
            orderBy: {
              createdAt: 'desc',
            },
          },
        },
      })

      if (!userWithPosts) {
        return res.status(404).json({ error: 'ユーザーが見つかりません' })
      }

      res.status(200).json(userWithPosts)
    } catch (error) {
      console.error(error)
      res.status(500).json({ error: 'データの取得に失敗しました' })
    }
  } else {
    res.setHeader('Allow', ['GET'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

APIのテスト方法

cURLを使ったテスト例

  1. ユーザー作成:
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"テストユーザー","email":"test@example.com"}'
  1. 投稿作成:
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"テスト投稿","content":"これはテストです","authorId":1}'
  1. ユーザーと投稿を取得:
curl http://localhost:3000/api/users/1/posts

ミドルウェアの活用

認証ミドルウェアの例

lib/middleware.tsを作成:

import { NextApiRequest, NextApiResponse } from 'next'

export const withAuth = (handler) => {
  return async (req: NextApiRequest, res: NextApiResponse) => {
    const authToken = req.headers.authorization

    if (!authToken || authToken !== process.env.API_SECRET) {
      return res.status(401).json({ error: '認証が必要です' })
    }

    return handler(req, res)
  }
}

ミドルウェアを使用したAPI

pages/api/protected.ts

import { NextApiRequest, NextApiResponse } from 'next'
import { withAuth } from '../../lib/middleware'

async function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: '認証済みのリクエストです' })
}

export default withAuth(handler)

エラーハンドリングの統一

lib/apiError.tsを作成:

export class ApiError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public details?: any
  ) {
    super(message)
  }
}

export const handleApiError = (
  error: unknown,
  res: NextApiResponse
) => {
  console.error(error)

  if (error instanceof ApiError) {
    return res.status(error.statusCode).json({
      error: error.message,
      details: error.details,
    })
  }

  if (error instanceof Error) {
    return res.status(500).json({
      error: '予期せぬエラーが発生しました',
      details: error.message,
    })
  }

  res.status(500).json({ error: '不明なエラーが発生しました' })
}

エラーハンドリングを活用したAPI

import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'
import { ApiError, handleApiError } from '../../../lib/apiError'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    if (req.method !== 'GET') {
      throw new ApiError(405, '許可されていないメソッドです')
    }

    const users = await prisma.user.findMany()

    if (!users.length) {
      throw new ApiError(404, 'ユーザーが見つかりません')
    }

    res.status(200).json(users)
  } catch (error) {
    handleApiError(error, res)
  }
}

バリデーションの実装

Zodを使ったリクエストバリデーション

npm install zod

lib/schemas.ts

import { z } from 'zod'

export const createUserSchema = z.object({
  name: z.string().min(2, '名前は2文字以上必要です'),
  email: z.string().email('有効なメールアドレスを入力してください'),
})

export const updatePostSchema = z.object({
  title: z.string().min(1, 'タイトルは必須です').optional(),
  content: z.string().min(1, '内容は必須です').optional(),
  published: z.boolean().optional(),
})

バリデーションを組み込んだAPI

import { NextApiRequest, NextApiResponse } from 'next'
import prisma from '../../../lib/prisma'
import { createUserSchema } from '../../../lib/schemas'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'POST') {
    try {
      const validatedData = createUserSchema.parse(req.body)

      const newUser = await prisma.user.create({
        data: validatedData,
      })

      res.status(201).json(newUser)
    } catch (error) {
      console.error(error)
      res.status(400).json({ 
        error: 'バリデーションエラー',
        details: error.errors 
      })
    }
  }
}

プロダクション環境でのベストプラクティス

  1. レートリミッティング:
   npm install rate-limiter-flexible

lib/rateLimiter.ts:

   import { RateLimiterMemory } from 'rate-limiter-flexible'

   export const rateLimiter = new RateLimiterMemory({
     points: 10, // 10リクエスト
     duration: 1, // 1秒あたり
   })
  1. CORS設定:
   npm install cors
   npm install --save-dev @types/cors

APIルートで使用:

   import Cors from 'cors'

   const cors = Cors({
     methods: ['GET', 'POST'],
   })

   function runMiddleware(req, res, fn) {
     return new Promise((resolve, reject) => {
       fn(req, res, (result) => {
         if (result instanceof Error) return reject(result)
         return resolve(result)
       })
     })
   }

   export default async function handler(req, res) {
     await runMiddleware(req, res, cors)
     // APIロジック
   }
  1. ロギング:
   export default async function handler(req, res) {
     console.log(`${req.method} ${req.url}`, {
       headers: req.headers,
       query: req.query,
       body: req.body,
     })

     // APIロジック
   }

まとめ

このガイドでは、Next.jsとPrismaを使用して本格的なREST APIを構築する方法を詳細に解説しました。基本的なCRUD操作から、認証、バリデーション、エラーハンドリングまで、実践的なAPI開発に必要な知識を網羅しています。

Next.jsのAPIルートは、フロントエンドとバックエンドを同じプロジェクトで管理できる強力な機能です。Prismaと組み合わせることで、データベース連携が必要なAPIも簡単に実装できます。

さらに学びを深めるには、以下のトピックに挑戦してみてください:

  • より複雑なリレーションの実装
  • GraphQL APIへの拡張
  • サーバーレス環境へのデプロイ
  • パフォーマンス最適化