Skip to content
Code Blue プログラミング学習ノート
Javascript/Next.js

Next.jsでPrismaを使ったREST API

はじめに

Next.jsとPrismaを組み合わせることで、フロントエンドとバックエンドを一体化した効率的なWebアプリケーション開発が可能になります。

本章では、Next.jsでREST APIを構築し、Prismaを利用してデータベースとの連携を行う基本的な流れを解説します。APIの設計からデータ操作、開発時のポイントまでを理解し、保守性の高いアプリケーション開発につなげていきます。

Next.js APIルートの基本

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

基本的なAPIルートの構造

このコードは、Next.jsのAPI Routesを利用してREST APIのエンドポイントを作成する例です。pages/api/example.tsに配置することで、/api/exampleへのHTTPリクエストを処理できます。

NextApiRequestとNextApiResponseを利用し、リクエストメソッドを判定します。GETリクエストの場合はJSON形式のレスポンスを返し、それ以外のメソッドには405エラーと許可されるHTTPメソッドを通知します。

// 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は、npmを使ってPrismaのクライアントライブラリとNext.jsをプロジェクトへ追加するコマンドです。

npm install @prisma/client next

@prisma/clientは、Prismaで定義したデータベースのモデルをアプリケーションから操作するためのライブラリです。データ取得や登録、更新、削除などのデータベース操作を、型安全なコードで実行できます。

nextは、ReactベースのフルスタックWebフレームワークであるNext.js本体をインストールするパッケージです。ページ表示やAPI Routes、サーバーサイド処理など、Webアプリケーション開発に必要な機能を提供します。

Prismaのセットアップ

このコマンドを実行することで、Next.jsアプリケーションからPrismaを利用してデータベースへアクセスできる環境を構築できます。prisma/schema.prismaに適切なモデルが定義されており、lib/prisma.tsでPrismaクライアントが初期化されている状態です。

ユーザーAPIの実装

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

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

このコードは、Next.jsのAPI RoutesとPrismaを利用して、データベースからユーザー情報を取得するREST APIの実装例です。prisma.user.findMany()を使用してユーザーテーブルのデータを取得し、selectで取得する項目を限定しています。

GETリクエストの場合は取得したデータをJSON形式で返却し、処理中にエラーが発生した場合は500エラーを返します。また、GET以外のHTTPメソッドには405エラーを返し、対応するメソッドを通知しています。

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)

次のコードは、Next.jsのAPI RoutesにPOST処理を追加し、Prismaを使って新しいユーザー情報をデータベースへ登録する実装例です。リクエストボディから名前とメールアドレスを取得し、必須項目が入力されているかを確認します。

prisma.user.create()によってユーザーデータを作成し、成功時には登録したデータを201ステータスで返却します。また、メールアドレスの重複エラー(P2002)を判定し、409エラーとして適切なメッセージを返す処理も含まれています。

// 既存の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のコードは、Next.jsのAPI RoutesとPrismaを利用して、指定したユーザーIDに対する取得・更新・削除処理を実装したREST APIです。

URLパラメータからユーザーIDを取得し、findUnique()で詳細情報を取得、update()でデータを更新、delete()でユーザーを削除します。HTTPメソッドごとに処理を分岐し、対象ユーザーが存在しない場合は404エラーを返します。また、Prisma固有のエラーコードを判定することで、重複や存在しないデータへの操作などを適切に処理しています。

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を作成します。このコードは、Next.jsのAPI RoutesとPrismaを利用して、投稿データを取得するREST APIの実装例です。GETリクエストを受け取ると、クエリパラメータのpublishedやauthorIdを条件として検索を行います。

findMany()では条件に一致する投稿を取得し、includeによって投稿者情報も同時に取得しています。また、orderByで作成日時の降順に並び替え、取得結果をJSON形式で返却します。エラー発生時には500エラーを返し、GET以外のリクエストは405エラーとして処理します。

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)

このコードは、Next.jsのAPI RoutesとPrismaを利用して、新しい投稿データを作成するPOST処理の実装例です。リクエストボディからタイトル、内容、公開状態、著者IDを取得し、必須項目の入力チェックを行います。

prisma.post.create()を使用して投稿を登録し、connectによって既存の著者データと関連付けています。登録成功時には投稿情報と著者情報を含めたJSONを201ステータスで返却します。また、指定した著者が存在しない場合はPrismaのエラーコードを判定し、404エラーとして処理します。

// 既存の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を作成します。このコードは、Next.jsのAPI RoutesとPrismaを利用して、指定したユーザーと関連する投稿データを取得するREST APIの実装例です。URLパラメータからユーザーIDを取得し、findUnique()で対象ユーザーを検索します。

includeを使用することで、ユーザー情報と紐づく投稿一覧を同時に取得し、公開済み(published: true)の投稿のみを対象にしています。また、投稿は作成日時の降順で並び替えられ、ユーザーが存在しない場合や処理中のエラーには適切なHTTPステータスを返します。

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を使ったテスト例

ユーザー作成

curlを使ってPOSTリクエストを送信し、ユーザー情報を登録するテストです。JSON形式で名前とメールアドレスを送信し、APIが正常にユーザーを作成できるか確認します。

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"テストユーザー","email":"test@example.com"}'

投稿作成

投稿作成用のAPIへPOSTリクエストを送信する例です。タイトル、本文、著者IDをJSONで渡し、Prismaを通じてデータベースへ投稿が登録されることを確認します。

curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"テスト投稿","content":"これはテストです","authorId":1}'

ユーザーと投稿を取得

指定したユーザーIDのユーザー情報と関連する投稿データを取得するGETリクエストです。ユーザーと投稿のリレーションが正しく取得できるか確認します。

curl http://localhost:3000/api/users/1/posts

ミドルウェアの活用

認証ミドルウェアの例

lib/middleware.tsを作成します。このコードは、Next.jsのAPI Routesに認証機能を追加するためのミドルウェア処理の実装例です。withAuth関数でAPIハンドラーをラップし、リクエストヘッダーのAuthorizationを確認します。

値が設定されていない場合や、環境変数API_SECRETの値と一致しない場合は401エラーを返し、不正なアクセスを防止します。認証に成功した場合のみ、元のAPI処理(handler)を実行します。

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を作成します。このコードは、認証ミドルウェアを適用したNext.js API Routesの実装例です。withAuthでAPIハンドラーをラップすることで、リクエスト処理の前に認証チェックを実行します。

正しい認証情報が含まれている場合のみhandler関数が呼び出され、「認証済みのリクエストです」というJSONレスポンスを返します。認証処理を共通化することで、複数のAPIエンドポイントで同じセキュリティ対策を簡単に利用できます。

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を作成します。このコードは、Next.jsのAPIで発生するエラーを統一的に処理するためのエラーハンドリング機能の実装例です。ApiErrorクラスを利用することで、HTTPステータスコードや詳細情報を含む独自エラーを作成できます。

handleApiError関数では、エラーの種類を判定し、適切なステータスコードとメッセージをJSON形式で返却します。APIごとに異なるエラー処理を書く必要がなくなり、コードの保守性や一貫性を向上させることができます。

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を使ったリクエストバリデーション

ZodとはTypeScript向けのデータ検証(バリデーション)ライブラリです。インストールコマンドは以下になります。

npm install zod

lib/schemas.tsを作成します。このコードは、zodを利用してAPIリクエストの入力値を検証するためのスキーマを定義しています。createUserSchemaでは、ユーザー作成時の名前とメールアドレスの形式をチェックし、条件を満たさない場合にエラーメッセージを返します。

updatePostSchemaでは、投稿更新時のタイトル・内容・公開状態を検証します。Zodによる入力チェックを共通化することで、不正なデータの登録を防ぎ、安全で保守性の高いAPIを構築できます。

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

以下のコードは、Next.jsのAPI Routesでユーザー作成処理に入力値検証を組み込んだ実装例です。リクエストボディのデータをcreateUserSchema.parse()で検証し、名前やメールアドレスが正しい形式であるか確認します。

検証に成功した場合のみ、Prismaのcreate()を使ってデータベースへユーザーを登録します。入力エラーが発生した場合は処理を中断し、400エラーと詳細なエラー情報を返却します。

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 
      })
    }
  }
}

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

レートリミッティング

rate-limiter-flexibleを利用して、APIへのリクエスト回数を制限する機能です。短時間に大量のアクセスが発生することを防ぎ、サーバー負荷や不正利用を軽減します。

   npm install rate-limiter-flexible

lib/rateLimiter.tsを作成してメモリ上でリクエスト数を管理する設定です。この例では、1秒間に10回までのリクエストを許可し、超過したアクセスを制限します。

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

   export const rateLimiter = new RateLimiterMemory({
     points: 10, // 10リクエスト
     duration: 1, // 1秒あたり
   })

CORS設定

corsパッケージを利用して、異なるドメインからAPIへアクセスする際の許可設定を行います。許可するHTTPメソッドを指定し、安全に外部アクセスを制御できます。

   npm install cors
   npm install --save-dev @types/cors

APIルートで使用してます。Express形式のミドルウェアをNext.jsのAPI Routesで利用するための補助関数です。CORSなどの処理を非同期処理として実行できるようにします。

   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ロジック
   }

API処理の前にCORSミドルウェアを実行し、許可されたリクエストのみAPIロジックへ進める仕組みです。

ロギング

APIへのリクエスト情報を記録する処理です。HTTPメソッド、URL、ヘッダー、パラメータ、送信データなどを確認でき、障害調査や動作確認に役立ちます。

   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も簡単に実装できます