nextjsprismatypescriptorm

Next.js + Prisma入門:型安全なWebアプリ開発の始め方

2025年6月28日に更新
価格
無料
❄️

はじめに:ORMとは何か?

現代のWebアプリケーション開発において、データベースとの連携は不可欠です。その際、多くの開発者が「ORM」という技術を利用します。

ORMは Object-Relational Mapping(オブジェクト関係マッピング) の略で、一言で言うと「プログラミング言語のオブジェクトと、リレーショナルデータベースのテーブルを対応付ける(マッピングする)技術」のことです。

通常、データベースを操作するにはSQL(Structured Query Language)を記述する必要があります。

SELECT id, name, email FROM "User" WHERE id = 1;

ORMを使うと、上記のようなSQL文を直接書く代わりに、使い慣れたプログラミング言語(この場合はTypeScript/JavaScript)のコードでデータベースを操作できます。

const user = await prisma.user.findUnique({
  where: { id: 1 },
});

ORMを利用するメリット

  • 生産性の向上: SQLを直接記述する必要がなくなり、より直感的かつ迅速にデータベース操作のコードを記述できます。
  • 安全性の向上: ORMがSQLを自動生成するため、SQLインジェクションのような脆弱性のリスクを低減できます。特に、後述するPrismaは「型安全性」に優れており、コンパイル時にエラーを発見しやすくなります。
  • コードの可読性と保守性の向上: SQLがコードから分離され、アプリケーションのロジックがより明確になります。
  • データベースの抽象化: 使用するデータベース(PostgreSQL, MySQLなど)が変更になっても、アプリケーション側のコードを大幅に変更する必要が少なくなります。

一方で、複雑なクエリではパフォーマンスが低下する可能性がある、ORMの学習コストがかかるといったデメリットも存在しますが、多くのアプリケーションではメリットが大きく上回ります。

Prisma入門:次世代のORM

Prismaは、Node.jsとTypeScript向けの「次世代ORM」と呼ばれています。他のORMと比較して、特に型安全性と**開発者体験(DX)**に重点を置いているのが大きな特徴です。

Prismaは、単一のライブラリではなく、主に3つのツールで構成されています。

  1. Prisma Client: 型安全なデータベースクライアントです。データベースのスキーマから自動生成され、補完が効くメソッドで直感的にクエリを記述できます。
  2. Prisma Migrate: 宣言的なマイグレーションツールです。SQLを書かずにschema.prismaというファイルでデータモデルを定義するだけで、データベースのテーブル構造を管理・変更できます。
  3. Prisma Studio: データベースのデータを視覚的に閲覧・編集できるGUIツールです。開発中にデータの状態を確認するのに非常に便利です。

これらのツールが連携し、データベースに関する開発体験を飛躍的に向上させます。

開発環境の準備:Next.js + Prisma

それでは、実際にNext.jsプロジェクトにPrismaを導入していきましょう。

1. Next.jsプロジェクトのセットアップ

まず、create-next-appを使ってTypeScriptベースのNext.jsプロジェクトを作成します。

npx create-next-app@latest nextjs-prisma-app --typescript
cd nextjs-prisma-app

2. Prisma CLIのインストール

次に、Prismaのコマンドラインツール(CLI)を開発者依存関係としてプロジェクトにインストールします。

npm install prisma --save-dev

3. Prismaの初期設定

以下のコマンドを実行して、Prismaの初期設定を行います。

npx prisma init

このコマンドを実行すると、プロジェクトのルートに以下のものが作成されます。

  • prisma ディレクトリ: この中に schema.prisma ファイルが生成されます。
  • .env ファイル: データベースの接続情報などを格納する環境変数ファイルです。

schema.prismaはPrismaの設定とデータモデルを定義する中心的なファイルで、.envはパスワードなどの機密情報をコードから分離するために使われます。.gitignore.envが追加されていることも確認しておきましょう。

データモデルの設計:schema.prisma

prisma/schema.prisma ファイルが、Prismaの心臓部です。ここでデータベース接続やデータモデルを定義します。

初期状態の schema.prisma はこのようになっています。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
  • generator client: Prisma Clientを生成するための設定です。
  • datasource db: 接続するデータベースの種類と接続URLを指定します。デフォルトはPostgreSQLですが、MySQLやSQLiteなども利用可能です。url.envファイル内のDATABASE_URLを参照するように設定されています。

.envファイルの設定

今回はローカル開発で手軽に試せるSQLiteを使用してみましょう。.envファイルを以下のように編集します。

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="file:./dev.db"

そして、schema.prismadatasourceブロックをSQLite用に変更します。

datasource db {
  provider = "sqlite" // "postgresql" から "sqlite" へ変更
  url      = env("DATABASE_URL")
}

モデルの定義

schema.prismamodelブロックを追加して、アプリケーションで使うデータモデルを定義します。ここでは、ブログアプリケーションを想定して「ユーザー」と「投稿」のモデルを定義してみましょう。

// (generatorとdatasourceの設定は省略)

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}
  • model Usermodel Postが、それぞれデータベースのUserテーブルとPostテーブルに対応します。
  • id Int @id @default(autoincrement()): idという名前の整数(Int)型のフィールドを定義し、@idで主キーに指定、@default(autoincrement())で自動採番されるように設定しています。
  • String, Boolean: フィールドの型を定義します。?を付けると、そのフィールドがオプショナル(NULLを許容)になります。
  • @unique: そのフィールドの値がテーブル内で一意であることを保証する制約です。
  • @relation: モデル間のリレーションを定義します。PostモデルのauthorフィールドはUserモデルと関連付いており、authorIdが外部キーとして機能します。
  • posts Post[]: Userモデル側から、そのユーザーが持つPostのリストにアクセスできるように定義しています。

データベースの構築:Prisma Migrate

スキーマの定義が完了したら、prisma migrateコマンドを使って、この定義を実際のデータベースに反映させます。

npx prisma migrate dev --name init
  • prisma migrate dev: 開発用のマイグレーションコマンドです。
  • --name init: このマイグレーションにinitという名前を付けています。

このコマンドを実行すると、Prismaは以下の処理を自動で行います。

  1. prisma/migrationsディレクトリに、スキーマの変更内容を記録したSQLファイル(マイグレーションファイル)を作成します。
  2. そのSQLファイルを実行して、データベース(今回はdev.dbというSQLiteファイル)にUserテーブルとPostテーブルを作成します。
  3. Prisma Clientを最新のスキーマに合わせて再生成します。

これで、定義したモデル通りのデータベースが構築されました。

データの操作:Prisma ClientのCRUD処理

データベースとやり取りするためのPrisma Clientが生成されたので、これを使ってデータの操作(CRUD: Create, Read, Update, Delete)をしてみましょう。

Prisma Clientのインスタンス化(Next.jsでのベストプラクティス)

Next.jsの開発サーバーはホットリロード(コード変更時に自動でリロード)機能を持っています。その際、何も対策をしないとリロードのたびにPrisma Clientの新しいインスタンスが大量に生成され、データベース接続を使い果たしてしまう可能性があります。

これを防ぐため、グローバルオブジェクトにPrisma Clientのインスタンスを保存し、シングルトン(常に単一のインスタンス)として利用するのがベストプラクティスとされています。

プロジェクトのルートにlib/prisma.tsというファイルを作成し、以下のコードを記述します。

import { PrismaClient } from '@prisma/client';

// グローバルオブジェクトにPrismaClientのインスタンスを保持するための型拡張
const globalForPrisma = global as unknown as {
  prisma: PrismaClient | undefined;
};

// prismaインスタンスをエクスポート
// production環境では常に新しいインスタンスを生成
// development環境ではグローバルオブジェクトに保存されたインスタンスを再利用
export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: ['query'], // 実行されるクエリをコンソールに出力する
  });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

これ以降、データベースを操作する際は、このlib/prisma.tsからprismaをインポートして使います。

Next.js API Routesでの実装例

Next.jsのAPI Routesを使って、ユーザーを作成し、一覧を取得するAPIを作成してみましょう。

ユーザー作成API (/api/users/create)

pages/api/users/create.ts

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

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method Not Allowed' });
  }

  try {
    const { email, name } = req.body;
    const newUser = await prisma.user.create({
      data: {
        email,
        name,
      },
    });
    res.status(201).json(newUser);
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
}

ユーザー一覧取得API (/api/users)

pages/api/users/index.ts

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

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    // findManyで全ユーザーを取得し、そのユーザーが持つ投稿(posts)も一緒に取得する
    const users = await prisma.user.findMany({
      include: {
        posts: true,
      },
    });
    res.status(200).json(users);
  } catch (error) {
    res.status(500).json({ message: 'Something went wrong' });
  }
}
  • prisma.user.create(): userテーブルに新しいレコードを作成します。
  • prisma.user.findMany(): userテーブルから複数のレコードを取得します。
  • include: { posts: true }: findManyfindUniqueなどのクエリで、リレーション関係にあるPostのデータも同時に取得するためのオプションです。

このように、Prisma ClientはTypeScriptの型定義に基づいており、prisma.user.まで入力するとcreate, findMany, updateなどの利用可能なメソッドがエディタ上で補完されます。これにより、タイプミスを防ぎ、非常に快適にコーディングを進めることができます。

開発を加速する:Prisma Studio

Prismaには、Prisma Studioという非常に強力なGUIツールが同梱されています。 ターミナルで以下のコマンドを実行してください。

npx prisma studio

ブラウザでhttp://localhost:5555が開き、下のような画面が表示されます。

Prisma Studio

この画面から、

  • UserPostモデル(テーブル)のデータを一覧表示
  • GUI操作でのレコードの追加、編集、削除
  • フィルタリングやソート

などが直感的に行えます。APIを実装しながら、データが正しく登録・更新されているかをリアルタイムで確認できるため、開発効率が劇的に向上します。

実践編:Next.jsページとの連携

最後に、作成したAPIをNext.jsのページコンポーネントから利用し、画面にデータを表示してみましょう。

ここでは、サーバーサイドでデータを取得するgetServerSidePropsを使います。

pages/users.tsxというファイルを作成します。

import type { GetServerSideProps, NextPage } from 'next';
import { prisma } from '../lib/prisma';
import { User, Post } from '@prisma/client';

// サーバーから渡されるpropsの型定義
// UserモデルにPostの配列が含まれる形にする
type UsersPageProps = {
  users: (User & {
    posts: Post[];
  })[];
};

// ページコンポーネント
const UsersPage: NextPage<UsersPageProps> = ({ users }) => {
  return (
    <div>
      <h1>Users</h1>
      {users.map((user) => (
        <div key={user.id} style={{ border: '1px solid #ccc', margin: '10px', padding: '10px' }}>
          <h2>{user.name} ({user.email})</h2>
          <h3>Posts:</h3>
          <ul>
            {user.posts.length > 0 ? (
              user.posts.map((post) => (
                <li key={post.id}>
                  <h4>{post.title}</h4>
                  <p>{post.content}</p>
                </li>
              ))
            ) : (
              <p>No posts yet.</p>
            )}
          </ul>
        </div>
      ))}
    </div>
  );
};

export default UsersPage;

// サーバーサイドで実行される関数
export const getServerSideProps: GetServerSideProps = async () => {
  // DBから直接データを取得する
  const users = await prisma.user.findMany({
    include: {
      posts: true, // ユーザーに関連する投稿も取得
    },
  });

  return {
    props: {
      users,
    },
  };
};

このコードでは、getServerSideProps内でPrisma Clientを直接使用して全ユーザーとその投稿を取得し、ページのpropsとして渡しています。クライアントサイドでは、渡されたusers配列をmapで展開して表示しています。

getServerSidePropsはページのリクエストごとにサーバー上で実行されるため、常に最新のデータベースの状態がページに反映されます。

この記事はいかがでしたか?

他にも様々な記事や本をご用意しています。ぜひチェックしてみてください🤩