awesome-hacks
Docs

NestJSでCRUD APIを作る(Prisma + SQLite)

PrismaでSQLiteに接続し、ユーザー管理のCRUD APIをController / Service / Moduleで一通り実装する

最終更新:2026/05/13

NestJSの基礎NestJS DTO / ValidationPipe 入門を終えている前提で進める。

この記事でやること

nestjs-practice-api(またはこれまでの api-sample)を データが残るAPI に育てる。

  • PrismaのセットアップとSQLite接続
  • User モデルとマイグレーション
  • CreateUserDtoDB設計に合わせて整理(メール一意・採番はDB側)
  • ServiceにCRUD、Controllerにルーティングを載せる

ここがシリーズの中核になる。以降の例外処理・認証・Swagger・テストは、このCRUDを前提に積み上がる。

NestJS DTO / ValidationPipe 入門から変えるポイント(意図)

入門の手順では学習用に id をクライアントから受け取るDTOにしていたことがある。
実務の「ユーザー作成」では、次が一般的だ。

  • id はDB(Prisma)が採番(cuid() や autoincrement など)
  • email は一意制約で重複を防ぐ
  • name は表示名

この記事では Create用DTOを email + name に寄せる。既存DTOがあるなら置き換えてよい。

技術構成(この記事の範囲)

  • NestJS
  • Prisma
  • SQLite(ファイル1つで完結。あとからPostgreSQL等へ差し替え可能)

ファイル構成(今回触る場所)

この記事では、Prisma関連のファイルと既存のusers配下のファイルを中心に触る。

api-sample/
├─ .env                          ← DATABASE_URLを書く
├─ prisma.config.ts              ← Prisma CLI用の接続設定(新規)
├─ prisma/
│  ├─ schema.prisma              ← DBモデルとPrisma設定(編集)
│  ├─ migrations/                ← マイグレーション履歴(自動生成)
│  └─ dev.db                     ← SQLiteのDBファイル(自動生成)
└─ src/
   ├─ generated/
   │  └─ prisma/                 ← Prisma Client(prisma generateで自動生成)
   ├─ prisma/
   │  ├─ prisma.service.ts       ← PrismaClientのNestJSラッパー(新規)
   │  └─ prisma.module.ts        ← PrismaModuleの定義(新規)
   ├─ users/
   │  ├─ dto/
   │  │  ├─ create-user.dto.ts   ← 作成用DTO(内容を整理)
   │  │  └─ update-user.dto.ts   ← 更新用DTO(新規)
   │  ├─ users.service.ts        ← CRUDのService実装(編集)
   │  └─ users.controller.ts     ← CRUDのController実装(編集)
   └─ app.module.ts              ← PrismaModuleを追加(編集)

src/generated/prisma/prisma/migrations/prisma/dev.db はコマンド実行で自動的に作られるため、自分で書くファイルではない。

サンプル実装手順

1. Prismaを入れる

Prisma 7はNode.js 20.19.0 以上が前提。先にバージョンを確認する。

node -v

v20.15.1 のような古い20系の場合は、Node.js 22系、または20.19以上へ上げてから進める。

cd nestjs-practice/api-sample   # 自分のプロジェクト名に合わせる

# npmの場合
npm install @prisma/client @prisma/adapter-better-sqlite3 better-sqlite3 dotenv
npm install prisma --save-dev

# pnpmの場合
pnpm add @prisma/client @prisma/adapter-better-sqlite3 better-sqlite3 dotenv
pnpm add -D prisma

pnpm add -D prisma でPrisma CLIをプロジェクトに入れた後は、pnpmでは pnpm exec prisma ... で実行する。pnpx prisma ... は一時実行用のパッケージを取りに行くため、プロジェクトに入っている prisma と別のものが動くことがある。NestJS側のコードで import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; も使うため、APIプロジェクトのルートpackage.json がある場所)で上の pnpm add ... または npm install ... を実行しておく。

pnpmの場合は次で初期化する。

pnpm exec prisma init

npmの場合は次。

npx prisma init

prisma/schema.prismaprisma.config.ts ができる。.env が無ければプロジェクトルートに作成する。

2. schema.prisma(SQLite)

prisma/schema.prisma を次のイメージにする(コメントは理解用)。

generator client {
  provider     = "prisma-client"
  output       = "../src/generated/prisma" // Prisma Clientの生成先
  moduleFormat = "cjs"                     // NestJS(CommonJS)向けの形式
}

datasource db {
  provider = "sqlite" // 使用するDB(接続URLはprisma.config.tsに書く)
}

model User {
  id        String   @id @default(cuid()) // 主キー(cuid形式で自動生成)
  email     String   @unique              // 一意制約
  name      String
  createdAt DateTime @default(now())      // 作成日時(自動セット)
  updatedAt DateTime @updatedAt           // 更新日時(自動更新)
}

Prisma 7では、schema.prismadatasourceurl = env("DATABASE_URL") を書かない。書いている場合は接続先URLは次の prisma.config.ts に移す。

generator clientoutput は、生成されるPrisma Clientの置き場所。ここではNestJS側から読みやすいように src/generated/prisma にしている。

moduleFormat = "cjs" は、生成されるPrisma ClientをCommonJS向けにする設定。NestJSの初期構成は package.json"type": "module" が無く、CommonJSとして実行されることが多い。その状態でPrisma ClientがESM形式のままだと、起動時に Cannot use 'import.meta' outside a module が出ることがあるため、この記事ではCJSに寄せる。

prisma.config.ts.env

Prisma CLI(migrate dev など)が使う接続先は、プロジェクトルートの prisma.config.ts に書く。Prisma 7では .env が自動では読まれないため、import "dotenv/config" も入れる。

import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  datasource: {
    url: env("DATABASE_URL"),
  },
});

ここで Cannot find module or type declarations for side-effect import of 'dotenv/config'. が出る場合は、プロジェクトに dotenv が入っていない可能性が高い。pnpmなら pnpm add dotenv、npmなら npm install dotenv を実行する。インストール済みなのにエディタだけが警告する場合は、TypeScript Serverの再起動で解消することがある。

.env はプロジェクトルートに置く。SQLiteで進める場合は、次のようにする。

DATABASE_URL="file:./prisma/dev.db"
  • **SQLiteの「DB」**は、専用サーバーを立てるのではなく、このパスに作られる1ファイル(中身はSQLite形式)。初回マイグレーションの前はファイルはまだ存在しないことが普通だ。
  • file:./prisma/dev.db は、プロジェクトルートから見て prisma/dev.db にSQLiteファイルを作る指定。prisma.config.ts とアプリ実行時の両方で同じパスとして扱いやすい。
  • pnpm exec prisma migrate dev を実行すると、この DATABASE_URL に沿って DBファイルが用意され、テーブル定義が反映される(同時に prisma/migrations に履歴が増える)。ここまでが「DBの準備」に相当する。
  • Gitでは、マイグレーションはコミットし、生成された *.db はチーム方針に応じて .gitignore に入れることが多い(ローカル専用DBとして捨てる想定なら無視でよい)。

prisma.config.ts はCLI用の設定、後で作る PrismaService はアプリ実行時用の設定、と分けて考えるとよい。どちらも同じ DATABASE_URL を見るようにしておくと、マイグレーション先とアプリの接続先がズレにくい。

3. マイグレーション

pnpm exec prisma migrate dev --name init_user
pnpm exec prisma generate

npmの場合は pnpm exec の代わりに npx を使う。

migrate devスキーマとDBファイルを同期し、generateTypeScriptから使うPrisma Client を生成する。
prisma/migrations が増え、src/generated/prisma が生成されていれば成功。

4. PrismaをNestに載せる(PrismaService)

Nest公式でもよく使われるパターンで、PrismaClient を1つにまとめてDIする。

src/prisma/prisma.service.ts

import "dotenv/config";
import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "../generated/prisma/client";

const adapter = new PrismaBetterSqlite3({
  url: process.env.DATABASE_URL ?? "file:./prisma/dev.db",
});

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  constructor() {
    super({ adapter });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

各部分の役割は次のとおり。

箇所役割
import "dotenv/config".env を読み込んで process.env.DATABASE_URL などを使えるようにする
OnModuleInit / OnModuleDestroyNestJSのModule起動時・終了時に処理を差し込むためのインターフェース
new PrismaBetterSqlite3({ url: ... })SQLiteへの接続設定を作る。DATABASE_URL がなければ file:./prisma/dev.db をデフォルトにする
extends PrismaClient継承することで this.user.findMany() などのメソッドをそのまま持つ
super({ adapter })親クラス(PrismaClient)にアダプターを渡して接続を初期化する
onModuleInit / $connect()Module起動時にDBへの接続を確立する
onModuleDestroy / $disconnect()アプリ終了時にDBとの接続を安全に切断する

このクラスの目的は「PrismaClient をNestJSのDIに乗せ、接続・切断をNestJSのライフサイクルに任せる」こと。各Serviceで constructor(private prisma: PrismaService) と書くだけでDBアクセスができるようになる。

ここで Cannot find module '@prisma/adapter-better-sqlite3' or its corresponding type declarations. が出る場合は、SQLite用adapterが未インストール。pnpmなら次をAPIプロジェクトのルートで実行する。

pnpm add @prisma/adapter-better-sqlite3 better-sqlite3

npmなら次。

npm install @prisma/adapter-better-sqlite3 better-sqlite3

package.json に追加されているのにエディタだけが警告する場合は、TypeScript Serverを再起動する。

src/prisma/prisma.module.ts

import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

@Global() にしておくと、各機能Moduleで imports を繰り返しにくい。規模が大きくなったら見直してもよい。

src/app.module.tsimportsPrismaModule を追加する。既に UsersModule がある場合は、次のように PrismaModule を import して配列に並べる。

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { PrismaModule } from "./prisma/prisma.module";
import { UsersModule } from "./users/users.module";

@Module({
  imports: [PrismaModule, UsersModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@Global() を付けていても、PrismaModule はアプリ内のどこかで一度は読み込む必要がある。ここではルートの AppModule に登録して、アプリ全体で PrismaService を使えるようにしている。

5. CreateUserDtoの整理

src/users/dto/create-user.dto.ts を例えば次の形にする(入門で付けた id フィールドは削除)。

import { IsEmail, IsString, Length } from "class-validator";

export class CreateUserDto {
  @IsEmail()
  email!: string;

  @IsString()
  @Length(1, 50)
  name!: string;
}

!definite assignment assertion。DTOは new CreateUserDto() のコンストラクタ内で値を代入するのではなく、NestJSがリクエストBodyからプロパティを詰める前提なので、strictPropertyInitialization が有効なプロジェクトでは付けておくとよい。

更新用は別DTOに分けるとルールが明確になる。例: src/users/dto/update-user.dto.ts

import { IsOptional, IsString, Length } from "class-validator";

export class UpdateUserDto {
  @IsOptional()
  @IsString()
  @Length(1, 50)
  name?: string;
}

ValidationPipewhitelist / forbidNonWhitelisted は、NestJS DTO / ValidationPipe 入門で設定したままでよい。

6. UsersService(CRUD)

ここで編集するファイルは api-sample/src/users/users.service.ts(プロジェクト名を変えている場合は src/users/users.service.ts)になる。要点だけ示す。戻り値の型は必要に応じて厳密化してよい。

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany({ orderBy: { createdAt: "desc" } });
  }

  findOne(id: string) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  create(dto: CreateUserDto) {
    return this.prisma.user.create({ data: dto });
  }

  update(id: string, dto: UpdateUserDto) {
    return this.prisma.user.update({ where: { id }, data: dto });
  }

  remove(id: string) {
    return this.prisma.user.delete({ where: { id } });
  }
}

7. UsersController(ルーティング)

ここで編集するファイルは api-sample/src/users/users.controller.ts(プロジェクト名を変えている場合は src/users/users.controller.ts)になる。ControllerはHTTPメソッドとURLをServiceの処理につなぐ場所。

import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { UsersService } from "./users.service";

@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return this.usersService.findOne(id);
  }

  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }

  @Patch(":id")
  update(@Param("id") id: string, @Body() dto: UpdateUserDto) {
    return this.usersService.update(id, dto);
  }

  @Delete(":id")
  remove(@Param("id") id: string) {
    return this.usersService.remove(id);
  }
}

@Get(":id")@Get() より 下に書くと安全(ルートの取り違え防止)。Nestのルート登録順の罠を避けたい場合は、静的パスを上にまとめる。

8. 動作確認

起動する。

pnpm run start:dev

npmで進めている場合は npm run start:dev でよい。以降の起動コマンドも、使っているパッケージマネージャに合わせて pnpm run / npm run を選ぶ。

別ターミナルから、例えば次で確認できる。

curl -s http://localhost:3000/users
curl -s -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"email":"taro@example.com","name":"Taro"}'

起動時に Cannot use 'import.meta' outside a module が出た場合は、Prisma ClientがESM形式で生成されている可能性が高い。schema.prismagenerator clientmoduleFormat = "cjs" が入っているか確認し、入っていなければ追加してから再生成する。

pnpm exec prisma generate
pnpm run start:dev

npmなら pnpm exec の代わりに npxpnpm run の代わりに npm run を使う。

起動時に better-sqlite3.node was compiled against a different Node.js versionNODE_MODULE_VERSION の不一致が出た場合は、better-sqlite3別のNode.jsバージョン向けにビルドされたまま になっている。better-sqlite3 はネイティブアドオンを含むため、Node.jsのバージョンを切り替えた後は再ビルドが必要になることがある。

まず現在のNode.jsバージョンを確認する。

node -v

pnpmなら次を実行する。

pnpm rebuild better-sqlite3
pnpm run start:dev

npmなら次。

npm rebuild better-sqlite3
npm run start:dev

それでも直らない場合は、今使っているNode.jsバージョンで依存関係を入れ直す。

rm -rf node_modules
pnpm install   # npmの場合は npm install
pnpm run start:dev

Prisma 7はNode.js 20.19.0以上が前提になるため、迷ったらNode.js 22系に揃えてから pnpm install し直すとトラブルが減りやすい。

同じ email で二度POSTすると、Prismaが一意制約違反を投げる。NestJSの例外処理HTTPとしてどう返すか を整える。

実装タスク(チェックリスト)

  • Prisma初期化・User モデル・DATABASE_URL
  • Prisma 7向けに schema.prisma から url を外し、prisma.config.tsdatasource.url を設定
  • NestJSのCommonJS実行に合わせて generator clientmoduleFormat = "cjs" を設定
  • SQLite用adapter(@prisma/adapter-better-sqlite3)を使って PrismaService を作成
  • prisma migrate devprisma generate
  • PrismaModule / PrismaServiceAppModule への登録
  • CreateUserDto / UpdateUserDto の整理
  • UsersService のCRUD
  • UsersController のルーティング
  • better-sqlite3NODE_MODULE_VERSION エラーが出た場合は、現在のNode.js向けにリビルド
  • curl で一覧・作成・取得・更新・削除を確認

参考