Contents
はじめにと前提条件
このセクションでは、本記事全体の対象読者と到達目標を簡潔に示します。NestJS と Prisma を組み合わせることで 型安全かつ高速なデータアクセス が実現でき、実務で即戦力になるコードベースが手に入ります。この記事を読み終えると、ローカル環境で動くサンプルリポジトリを自分の手で構築し、GitHub Actions による CI/CD まで走らせられるようになります。
対象読者
- NestJS を学び始めたばかりで、フレームワークの基本的な使い方を知りたい人
- フロントエンド中心に開発してきて、バックエンドの型安全性やテスト手法を身につけたい人
この記事のゴール
- NestJS プロジェクトへ Prisma v5 を正しく組み込む(DI・リクエストスコープ)
- Article エンティティで CRUD API を完成させる(DTO・バリデーション)
- Jest と GitHub Actions でテスト&マイグレーションを自動化
NestJS プロジェクトの作成と環境構築
この章では、NestJS の雛形作成から TypeScript の設定確認までを順に行います。最初に開発基盤を整えることで、以降の手順がスムーズに進みます。
Node.js とパッケージマネージャのインストール
Node.js LTS (v20 系) を公式サイトからインストールし、pnpm をグローバルに追加します。pnpm は依存解決が高速でディスク使用量も抑えられるため、モノレポや CI 環境でも好評です。
|
1 2 3 4 5 6 7 |
# npm が入っている前提で pnpm をインストール npm i -g pnpm # バージョン確認(必ず LTS と最新の pnpm が出ること) node -v # => v20.x pnpm -v # => 9.x 以上 |
Nest CLI のセットアップとプロジェクト生成
Nest CLI はプロジェクト雛形やモジュール自動生成を支援します。以下のコマンドで新規アプリケーションを作成し、パッケージマネージャは pnpm を選択してください。
|
1 2 3 |
pnpm add -g @nestjs/cli nest new nest-prisma-demo # プロンプトで「pnpm」を選択 |
生成されたディレクトリ構造(主要ファイル)を示します。src/ 以下に実装コードが集約されます。
|
1 2 3 4 5 6 7 8 9 10 11 |
nest-prisma-demo/ ├─ src/ │ ├─ app.module.ts │ ├─ main.ts │ └─ ...(後述のモジュール・コントローラがここに入る) ├─ test/ # e2e テスト用ディレクトリ ├─ prisma/ # Prisma の設定ファイルが格納される ├─ .env # 環境変数(DB 接続文字列など)を記述 ├─ tsconfig.json └─ package.json |
TypeScript 設定の確認
NestJS が生成する tsconfig.json は strict モード が有効です。この設定がオンになっていると PrismaClient の型情報が完全に補完され、開発時のミスを大幅に減らせます。
|
1 2 3 4 5 6 7 8 9 10 11 |
{ "compilerOptions": { "module": "commonjs", "target": "es2022", "strict": true, "noImplicitAny": true, "sourceMap": true, "outDir": "./dist" } } |
Prisma の導入と設定
Prisma を NestJS に組み込む際の鍵は PrismaService(ラップしたクラス)をリクエストスコープで提供し、モジュール間で DI できるようにすることです。この章ではインストールからマイグレーション、サービス実装までを網羅します。
Prisma のインストールと初期化
開発依存として prisma とクライアントパッケージ @prisma/client を追加し、prisma init で雛形ファイルを作ります。
|
1 2 3 |
pnpm add -D prisma @prisma/client npx prisma init # prisma/ ディレクトリと schema.prisma, .env が生成される |
.env にデータベース接続情報を記載
本稿では PostgreSQL 15 を想定します。ローカル開発用 DB と、マイグレーション時に Prisma が内部で使用する shadow database の二つの URL を環境変数として管理します。
|
1 2 3 4 |
# .env(プロジェクト直下) DATABASE_URL="postgresql://user:password@localhost:5432/nest_prisma_demo?schema=public" SHADOW_DATABASE_URL="postgresql://user:password@localhost:5432/shadow_nest_prisma_demo?schema=public" |
重要: shadow database には
CREATEDB権限が必要です。PostgreSQL の公式ドキュメント(Shadow Database – Prisma Docs)を参照し、権限付与を忘れずに行ってください。
スキーマ定義とマイグレーション実行
prisma/schema.prisma に Article モデルを記述します。ファイル全体を掲載し、インデントや属性の意味も簡単に解説します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } // アプリケーションで扱う記事テーブル model Article { id Int @id @default(autoincrement()) title String @db.VarChar(255) content String? published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } |
スキーマ保存後、ローカル DB にマイグレーションを適用します。
|
1 2 3 |
npx prisma migrate dev --name init # 開発環境向け。shadow DB が自動で利用される # → migration ファイルが prisma/migrations/ に生成され、@prisma/client の型定義も更新されます。 |
補足:
prisma generateはmigrate devの実行時に自動で走りますので手動は不要です。
PrismaService の実装(リクエストスコープ)
公式ガイド(NestJS Request‑Scoped Providers)を参考に、PrismaClient を継承したサービスを作ります。Scope.REQUEST にすることで、トランザクションやミドルウェアでリクエスト単位の状態管理がしやすくなります。
|
1 2 3 4 5 6 7 8 9 10 11 |
// src/prisma/prisma.service.ts import { Injectable, OnModuleDestroy, Scope } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable({ scope: Scope.REQUEST }) export class PrismaService extends PrismaClient implements OnModuleDestroy { async onModuleDestroy() { await this.$disconnect(); // アプリ終了時に接続を解放 } } |
次に、他モジュールからインジェクションできるように PrismaModule を作成します。
|
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 {} |
アプリ全体への PrismaModule の組み込み
AppModule に PrismaModule をインポートすれば、どのモジュールでも PrismaService が注入できるようになります。
|
1 2 3 4 5 6 7 8 9 10 |
// src/app.module.ts import { Module } from '@nestjs/common'; import { ArticlesModule } from './articles/articles.module'; import { PrismaModule } from './prisma/prisma.module'; @Module({ imports: [PrismaModule, ArticlesModule], }) export class AppModule {} |
CRUD API の実装
ここからは Article エンティティを例に、DTO・バリデーション・サービス・コントローラの実装手順を示します。各コードブロックにはファイルパスと簡単な説明を書き添えているので、初心者でも全体像が掴みやすくなっています。
DTO(Data Transfer Object)の作成
DTO は class-validator と組み合わせてリクエストボディのバリデーションを行います。まずは記事作成用 DTO を定義します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/articles/dto/create-article.dto.ts import { IsString, IsNotEmpty, MaxLength, IsOptional, IsBoolean, } from 'class-validator'; export class CreateArticleDto { @IsString() @IsNotEmpty() @MaxLength(255) title: string; @IsString() @IsOptional() content?: string; @IsBoolean() @IsOptional() published?: boolean; } |
更新用 DTO は PartialType を活用して、全フィールドを任意にできます。
|
1 2 3 4 5 6 |
// src/articles/dto/update-article.dto.ts import { PartialType } from '@nestjs/mapped-types'; import { CreateArticleDto } from './create-article.dto'; export class UpdateArticleDto extends PartialType(CreateArticleDto) {} |
ArticlesService の実装(CRUD ロジック)
PrismaService を注入し、各メソッドで PrismaClient の API を呼び出します。エラーハンドリングは NestJS 標準の例外クラスを使用し、統一されたレスポンスフォーマットを実現しています。
|
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 |
// src/articles/articles.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateArticleDto } from './dto/create-article.dto'; import { UpdateArticleDto } from './dto/update-article.dto'; @Injectable() export class ArticlesService { constructor(private readonly prisma: PrismaService) {} async create(dto: CreateArticleDto) { return this.prisma.article.create({ data: dto }); } async findAll() { return this.prisma.article.findMany(); } async findOne(id: number) { const article = await this.prisma.article.findUnique({ where: { id } }); if (!article) throw new NotFoundException(`Article #${id} not found`); return article; } async update(id: number, dto: UpdateArticleDto) { return this.prisma.article.update({ where: { id }, data: dto, }); } async remove(id: number) { await this.prisma.article.delete({ where: { id } }); return { message: `Deleted article #${id}` }; } } |
ArticlesController のエンドポイント定義
ParseIntPipe を使うことで URL パラメータの型変換が自動的に行われ、コード内で数値として扱えるようになります。
|
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 |
// src/articles/articles.controller.ts import { Controller, Get, Post, Body, Param, Patch, Delete, ParseIntPipe, } from '@nestjs/common'; import { ArticlesService } from './articles.service'; import { CreateArticleDto } from './dto/create-article.dto'; import { UpdateArticleDto } from './dto/update-article.dto'; @Controller('articles') export class ArticlesController { constructor(private readonly articlesService: ArticlesService) {} @Post() create(@Body() dto: CreateArticleDto) { return this.articlesService.create(dto); } @Get() findAll() { return this.articlesService.findAll(); } @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.articlesService.findOne(id); } @Patch(':id') update( @Param('id', ParseIntPipe) id: number, @Body() dto: UpdateArticleDto, ) { return this.articlesService.update(id, dto); } @Delete(':id') remove(@Param('id', ParseIntPipe) id: number) { return this.articlesService.remove(id); } } |
ArticlesModule の結合
ArticlesModule が PrismaModule とサービス・コントローラを束ねます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// src/articles/articles.module.ts import { Module } from '@nestjs/common'; import { ArticlesService } from './articles.service'; import { ArticlesController } from './articles.controller'; import { PrismaModule } from '../prisma/prisma.module'; @Module({ imports: [PrismaModule], providers: [ArticlesService], controllers: [ArticlesController], }) export class ArticlesModule {} |
テスト・CI/CD とデプロイ
本番環境へ安全にリリースするには、ユニットテストと e2e テストを自動化し、GitHub Actions 上でマイグレーションを走らせることが必須です。以下では Jest のモック戦略と、CI パイプライン全体像を示します。
Jest でのユニットテスト(サービス層)
PrismaClient は外部 DB に依存するため、テスト時は モックオブジェクトに差し替えて高速化します。test/articles.service.spec.ts の全容です。
|
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 |
// test/articles.service.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { ArticlesService } from '../src/articles/articles.service'; import { PrismaService } from '../src/prisma/prisma.service'; describe('ArticlesService', () => { let service: ArticlesService; const mockPrisma = { article: { create: jest.fn().mockResolvedValue({ id: 1, title: 'Test', published: false }), findMany: jest.fn().mockResolvedValue([]), findUnique: jest.fn(), update: jest.fn(), delete: jest.fn(), }, }; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ArticlesService, { provide: PrismaService, useValue: mockPrisma }, ], }).compile(); service = module.get<ArticlesService>(ArticlesService); }); it('should create an article', async () => { const dto = { title: 'Test' }; const result = await service.create(dto as any); expect(result).toHaveProperty('id'); expect(mockPrisma.article.create).toBeCalledWith({ data: dto }); }); // …他の CRUD メソッドも同様にテスト可能 }); |
e2e テスト(実 DB を使用)
Docker 上の PostgreSQL コンテナを起動し、実際のマイグレーションとエンドポイント呼び出しを行います。
|
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 |
# .github/workflows/ci.yml(抜粋) name: CI on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:15-alpine env: POSTGRES_USER: user POSTGRES_PASSWORD: password POSTGRES_DB: nest_prisma_test 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: Set up Node uses: actions/setup-node@v3 with: node-version: '20' cache: pnpm - run: pnpm install # テスト用 .env を作成 - name: Write test env file run: | echo "DATABASE_URL=postgresql://user:password@localhost:5432/nest_prisma_test?schema=public" > .env.test echo "SHADOW_DATABASE_URL=postgresql://user:password@localhost:5432/shadow_nest_prisma_test?schema=public" >> .env.test # Prisma v5 では preview フラグは不要 → 正しいコマンドに修正 - name: Run migrations run: pnpm prisma migrate deploy --schema=prisma/schema.prisma --skip-generate - name: Run e2e tests run: pnpm test:e2e |
変更点のポイント
pnpm prisma migrate deployに preview フラグを削除(Prisma v5 では不要)--schemaと--skip-generateを付与し、CI 時の余計なビルド時間を短縮
CI パイプライン全体像とデプロイ手順
- シークレット管理:GitHub の Settings → Secrets に
DATABASE_URL・SHADOW_DATABASE_URLを登録。 - マイグレーション実行:上記の
prisma migrate deployが失敗したらジョブは即座に停止します。 - ビルド & デプロイ:テストがすべて成功したら、Docker イメージを作成し Render・Fly.io などの PaaS にプッシュするステップを追加できます(本稿では割愛)。
補足情報: 本番環境でも shadow database が必要です。DB 管理者に
CREATEDB権限付与を依頼してください(公式ドキュメント参照:https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database)。
まとめと次のアクション
この記事で取り上げた内容は、NestJS v10 + Prisma v5 の実務レベル構成です。ポイントを再度整理すると以下の通りです。
| 項目 | 内容 |
|---|---|
| 開発基盤 | Node.js LTS + pnpm + Nest CLI でプロジェクト作成 |
| DB 設定 | .env に DATABASE_URL と SHADOW_DATABASE_URL を記載し、shadow DB に CREATEDB 権限を付与 |
| Prisma 導入 | prisma init → スキーマ作成 → prisma migrate dev でマイグレーション |
| サービス層 | PrismaService(リクエストスコープ)を作り、DI で注入 |
| CRUD 実装 | DTO + class‑validator + Service + Controller の標準構成 |
| テスト | Jest でユニットテスト、Docker PostgreSQL で e2e テスト |
| CI/CD | GitHub Actions でマイグレーション自動実行(prisma migrate deploy) → テスト → デプロイ |
今すぐできること
- 本リポジトリをクローンし、
pnpm install && pnpm prisma migrate devをローカルで走らせる。 npm run start:devで API が起動したら、Postman や VSCode の REST Client でエンドポイント (/articles) を叩く。- GitHub にプッシュし、Actions タブで CI が成功することを確認。
次のステップ: 認証・認可(Passport, JWT)や GraphQL への移行、またはマイクロサービス化を検討すると、更なるスケーラビリティが得られます。
参考リンク
- Prisma Docs – Shadow Database: https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database
- NestJS Official Docs – Request‑Scoped Providers: https://docs.nestjs.com/faq/request-scope
- Jest Documentation: https://jestjs.io/docs/getting-started
- GitHub Actions 官方ガイド: https://docs.github.com/en/actions
これで 型安全・テスト自動化された NestJS + Prisma バックエンド が完成です。ぜひ手元で動かして、実務プロジェクトにすぐ適用してください!