Contents
開発環境と前提条件
このセクションでは、NestJS 10 と Prisma 5 を組み合わせたプロジェクトをローカルで即座に立ち上げるための必須要件をまとめます。Node のバージョン選定やデータベースの用意方法を明示することで、環境構築時の「どれが足りない?」という疑問を防ぎます。以下の手順に従えば、数分で開発可能な状態になります。
Node.js とパッケージマネージャ
- 推奨バージョン: Node 20 以上(LTS)
- 推奨ツール:
pnpm(高速・ディスク節約)またはyarn
ポイント: LTS版は ESモジュールや型定義が安定しているため、NestJS のビルドエラーを減らせます。
実装例:
|
1 2 3 4 5 6 |
# nvm で Node20 をインストールし有効化 nvm install 20 && nvm use 20 # pnpm のグローバルインストール npm i -g pnpm |
データベースの準備(PostgreSQL / MySQL)
Docker コンテナでデータベースを起動すれば、OS 間の差異やバージョン衝突を回避できます。.env に接続文字列だけを書き換えることで、本番環境への移行もシームレスです。
ポイント: コンテナ化された DB はチーム全体で同一設定を共有でき、CI でも同様のイメージを使用できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# docker-compose.yml(最小構成) version: '3.8' services: db: image: postgres:15-alpine # MySQL を使う場合は mysql:8 に置き換え environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: nest_prisma_demo ports: - "5432:5432" |
|
1 2 3 |
docker compose up -d # 起動 psql -h localhost -U user -d nest_prisma_demo # 接続確認(PostgreSQL の場合) |
NestJS CLI のインストール
NestJS 10 用のプロジェクトは公式 CLI で生成すると、推奨されるディレクトリ構成や設定ファイルが自動で作成されます。
|
1 2 3 |
pnpm add -g @nestjs/cli nest new nest-prisma-demo --package-manager pnpm |
src/app.module.ts が作成されたことを確認したら、次のステップへ進みましょう。
Prisma のセットアップとスキーマ定義
この章では Prisma 5 用 CLI とクライアントの導入手順、そしてデータモデルを記述する schema.prisma の基本構造をご紹介します。公式ドキュメントは頻繁に更新されるため、プレビュー機能(例: $transaction)の利用可否は必ず最新版で確認してください。
必要パッケージのインストール
| パッケージ | 用途 | 推奨インストールコマンド |
|---|---|---|
prisma (devDependency) |
スキーマ生成・マイグレーション CLI | pnpm add -D prisma |
@prisma/client (dependency) |
実行時に使用する型安全クライアント | pnpm add @prisma/client |
|
1 2 |
npx prisma init # 初回実行で prisma/ ディレクトリと .env.example が生成されます |
ポイント: 1 行のコマンドですべての雛形が作成され、以後は
prisma/schema.prismaと.envのみを編集すれば OK です。
ディレクトリ構造(npx prisma init 後)
prisma/schema.prisma…データモデルと datasource 設定.env…環境変数(DB 接続文字列)を格納prisma/migrations/…自動生成されるマイグレーション履歴
データソースとジェネレータの設定例
|
1 2 3 4 5 6 7 8 9 10 11 |
datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" // プレビュー機能はバージョンに依存します。利用前に公式ドキュメントで確認してください。 previewFeatures = ["clientExtensions"] // $transaction はオプションです } |
.env(シークレット管理のベストプラクティス):
|
1 2 3 |
# .env.example にサンプルを残し、実環境では CI/CD のシークレット機能で上書きします。 DATABASE_URL="postgresql://user:pass@localhost:5432/nest_prisma_demo?schema=public" |
注意:
$transactionは Prisma 5 のプレビュー機能の一例です。バージョンやリリースタイミングにより利用不可になる場合があります。必ず最新版ドキュメントで確認してください。
NestJS への Prisma 統合
PrismaClient を Nest の DI コンテナに組み込むだけで、アプリ全体から型安全に DB アクセスできます。この章では サービス層の実装 と モジュール化手順 を解説し、Graceful Shutdown(安全な終了処理)も網羅します。
PrismaService の実装
PrismaService は PrismaClient を継承し、Nest のライフサイクルフックで接続と切断を自動管理します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/prisma/prisma.service.ts import { Injectable, OnModuleInit, BeforeApplicationShutdown } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, BeforeApplicationShutdown { async onModuleInit() { await this.$connect(); } async beforeApplicationShutdown(signal?: string) { await this.$disconnect(); } } |
ポイント:
OnModuleInitとBeforeApplicationShutdownを実装するだけで、サーバ起動時に自動接続・停止時に安全切断が保証されます。
PrismaModule の作成と AppModule への組み込み
|
1 2 3 4 5 6 7 8 9 10 |
// src/prisma/prisma.module.ts import { Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { PrismaModule } from './prisma/prisma.module'; import { UserModule } from './user/user.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), // .env の自動読み込み PrismaModule, UserModule, ], }) export class AppModule {} |
ポイント:
ConfigModuleと併用すれば、環境変数はアプリ全体でシームレスに利用できます。
Feature Module における CRUD 実装とベストプラクティス
本節では「ユーザー管理」機能を例に、サービス層の設計、REST / GraphQL のハンドラ、トランザクション活用、エラー統一処理までを網羅します。コードは シンプルかつ再利用可能 になるよう意識しています。
UserService(ビジネスロジック層)
- PrismaClient を DI で注入
- 型安全な CRUD メソッドを提供
- エラーハンドリングは
HttpExceptionに統一
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
// src/user/user.service.ts import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { User, Prisma } from '@prisma/client'; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} async create(data: Prisma.UserCreateInput): Promise<User> { try { return await this.prisma.user.create({ data }); } catch (e) { throw new HttpException('ユーザー作成に失敗しました', HttpStatus.BAD_REQUEST); } } async findMany(): Promise<User[]> { return this.prisma.user.findMany(); } async update(id: number, data: Prisma.UserUpdateInput): Promise<User> { try { return await this.prisma.user.update({ where: { id }, data }); } catch (e) { throw new HttpException('ユーザー更新に失敗しました', HttpStatus.NOT_FOUND); } } async delete(id: number): Promise<User> { try { return await this.prisma.user.delete({ where: { id } }); } catch (e) { throw new HttpException('ユーザー削除に失敗しました', HttpStatus.NOT_FOUND); } } // トランザクション例(プレビュー機能は有効か必ず確認) async createWithProfile( userData: Prisma.UserCreateInput, profileData: Prisma.ProfileCreateInput, ) { return this.prisma.$transaction(async (tx) => { const user = await tx.user.create({ data: userData }); await tx.profile.create({ data: { ...profileData, userId: user.id }, }); return user; }); } } |
REST コントローラ
UserServiceを直接呼び出すだけで完結- パスパラメータは数値に変換して型安全性を保つ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// src/user/user.controller.ts import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common'; import { UserService } from './user.service'; import { Prisma } from '@prisma/client'; @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @Post() create(@Body() data: Prisma.UserCreateInput) { return this.userService.create(data); } @Get() findAll() { return this.userService.findMany(); } @Put(':id') update(@Param('id') id: string, @Body() data: Prisma.UserUpdateInput) { return this.userService.update(+id, data); } @Delete(':id') remove(@Param('id') id: string) { return this.userService.delete(+id); } } |
GraphQL リゾルバ
- 同一サービスを再利用し、型は
@nestjs/graphqlのデコレータで自動生成 - 引数は GraphQL スキーマに合わせて明示的に宣言
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// src/user/user.resolver.ts import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql'; import { UserService } from './user.service'; import { User as PrismaUser, Prisma } from '@prisma/client'; @Resolver('User') export class UserResolver { constructor(private readonly userService: UserService) {} @Query(() => [PrismaUser]) users() { return this.userService.findMany(); } @Mutation(() => PrismaUser) createUser(@Args('data') data: Prisma.UserCreateInput) { return this.userService.create(data); } @Mutation(() => PrismaUser) updateUser( @Args('id', { type: () => Int }) id: number, @Args('data') data: Prisma.UserUpdateInput, ) { return this.userService.update(id, data); } @Mutation(() => PrismaUser) deleteUser(@Args('id', { type: () => Int }) id: number) { return this.userService.delete(id); } } |
エラーハンドリングの統一
Prisma が投げる例外は内部情報が漏れやすいため、Nest の HttpException にマッピングします。
|
1 2 3 4 5 6 7 8 9 10 |
import { Prisma } from '@prisma/client'; import { HttpException, HttpStatus } from '@nestjs/common'; function mapPrismaError(error: unknown) { if (error instanceof Prisma.PrismaClientKnownRequestError) { return new HttpException(error.message, HttpStatus.BAD_REQUEST); } return new HttpException('内部サーバーエラー', HttpStatus.INTERNAL_SERVER_ERROR); } |
ポイント: 例外変換はサービス層で行うか、グローバルな
ExceptionFilterに委譲するとコードがさらにシンプルになります。
マイグレーション・シード・テスト・CI/CD の実践
本章では、開発フロー全体を通した データベースのバージョン管理と自動化 を解説します。マイグレーションの作成から CI での検証まで、一連の手順が揃っていれば、本番リリース時に「スキーマが合わない」問題は起きません。
マイグレーションの実行(prisma migrate dev)
- スキーマ変更後は必ず
prisma migrate devでローカル DB に適用 - コマンドは自動的に SQL を生成し、
prisma/migrations/に履歴を保存
|
1 2 3 |
# 例: User テーブルに email カラムを追加したとき npx prisma migrate dev --name add-email-to-user |
ポイント: マイグレーションは Git の管理対象になるので、コードレビューで変更点を確認できます。
シードスクリプトの作成
開発・テスト環境で同一データセットを自動投入することで、手動入力ミスや環境差異を防ぎます。以下は TypeScript 版シードです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// prisma/seed.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { await prisma.user.createMany({ data: [ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' }, ], skipDuplicates: true, }); } main() .catch((e) => console.error(e)) .finally(() => prisma.$disconnect()); |
package.json にスクリプトを追加し、マイグレーションとシードを一括実行できるようにします。
|
1 2 3 4 5 6 7 |
{ "scripts": { "migrate:dev": "prisma migrate dev && pnpm seed", "seed": "ts-node prisma/seed.ts" } } |
テスト環境の構築
| テスト種別 | 推奨手法 | 主な利点 |
|---|---|---|
| ユニットテスト | Prisma クライアントを Jest のモックに置き換える | 外部 DB に依存せず高速 |
| E2E テスト | SQLite メモリデータベース (file:./test.db?mode=memory&cache=shared) を使用 |
実際の SQL が走るのでロジック検証が正確 |
ユニットテスト例(Jest)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// user.service.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { UserService } from './user.service'; import { PrismaService } from '../prisma/prisma.service'; const prismaMock = { user: { create: jest.fn(), findMany: jest.fn(), update: jest.fn(), delete: jest.fn(), }, }; describe('UserService', () => { let service: UserService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: PrismaService, useValue: prismaMock }, ], }).compile(); service = module.get<UserService>(UserService); }); // 各メソッドのテストを記述 … }); |
E2E テストで SQLite を使用する例
.env.test に以下を設定し、GitHub Actions のジョブで読み込むだけです。
|
1 2 |
DATABASE_URL="file:./test.db?mode=memory&cache=shared" |
テストスクリプトは通常通り pnpm test:e2e で実行可能です。
CI/CD(GitHub Actions)での自動化
CI では シークレット管理 を徹底し、ハードコーディングされた接続文字列を排除します。以下は PR 時にマイグレーション・シード・テストまで実行するワークフローです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
name: CI on: push: branches: [main] pull_request: jobs: build-test: runs-on: ubuntu-latest services: db: image: postgres:15-alpine env: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: nest_prisma_demo ports: ['5432:5432'] options: >- --health-cmd "pg_isready -U user" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: '20' cache: pnpm - run: pnpm install # 環境変数は GitHub Secrets から取得(ハードコーディング回避) - name: Create .env from secrets run: | echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" > .env - name: Run migrations & seed run: pnpm migrate:dev # 事前に `prisma migrate dev && pnpm seed` が実行される - name: Test run: pnpm test |
ポイント:
DATABASE_URLはリポジトリ上に平文で残さず、GitHub の Secrets から注入します。これがシークレット管理のベストプラクティスです。
カスタマイズとブランド適合性
この記事は汎用的な構成を示していますが、企業やサービス独自のトーン・ブランディングに合わせて調整可能です。たとえば:
- ロゴやカラーコードを README のバッジに組み込む
- 社内ガイドラインで定められた用語(例: “ユーザー” → “顧客”)に置換
- CI/CD では自社のシークレット管理ツール(AWS Secrets Manager、HashiCorp Vault 等)を使用するよう書き換える
上記項目はプロジェクトの docs/README.md や内部ウィキでテンプレート化すると、チーム全体で統一感のあるドキュメントが保てます。
まとめ
- 開発環境: Node 20+pnpm、Docker 化した PostgreSQL/MySQL、NestJS CLI による即時プロジェクト生成
- Prisma 導入:
prismaと@prisma/clientのインストール、.envで接続情報を管理。プレビュー機能は必ず公式ドキュメントで可用性確認。 - NestJS 統合:
PrismaService(ライフサイクルフック)とPrismaModuleにより DI が完了し、全モジュールから型安全に DB へアクセス可能。 - CRUD 実装: サービス層でロジックを集中管理し、REST と GraphQL のハンドラは同一サービスを再利用。トランザクションは
$transaction(プレビュー機能)で実装例示。エラーはHttpExceptionに統一。 - マイグレーション・シード・テスト・CI:
prisma migrate dev→ 自動シード、ユニットテストはモック、E2E は SQLite メモリ DB、GitHub Actions では Secrets を用いた安全な環境変数注入で全工程を自動化。
これらの手順を踏めば、型安全・スケーラブルかつ運用コストが低減した NestJS 10 + Prisma 5 アプリケーション がすぐに構築できます。必要に応じてブランド固有の表現や社内ツールチェーンへ置き換えてご活用ください。