NestJSでCRUD APIを作る(Prisma + SQLite)
PrismaでSQLiteに接続し、ユーザー管理のCRUD APIをController / Service / Moduleで一通り実装する
NestJSの基礎とNestJS DTO / ValidationPipe 入門を終えている前提で進める。
この記事でやること
nestjs-practice-api(またはこれまでの api-sample)を データが残るAPI に育てる。
- PrismaのセットアップとSQLite接続
UserモデルとマイグレーションCreateUserDtoを DB設計に合わせて整理(メール一意・採番は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 -vv20.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 prismapnpm 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 initnpmの場合は次。
npx prisma initprisma/schema.prisma と prisma.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.prisma の datasource に url = env("DATABASE_URL") を書かない。書いている場合は接続先URLは次の prisma.config.ts に移す。
generator client の output は、生成される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 generatenpmの場合は pnpm exec の代わりに npx を使う。
migrate dev で スキーマとDBファイルを同期し、generate で TypeScriptから使う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 / OnModuleDestroy | NestJSの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-sqlite3npmなら次。
npm install @prisma/adapter-better-sqlite3 better-sqlite3package.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.ts の imports に PrismaModule を追加する。既に 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;
}ValidationPipe の whitelist / 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:devnpmで進めている場合は 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.prisma の generator client に moduleFormat = "cjs" が入っているか確認し、入っていなければ追加してから再生成する。
pnpm exec prisma generate
pnpm run start:devnpmなら pnpm exec の代わりに npx、pnpm run の代わりに npm run を使う。
起動時に better-sqlite3.node was compiled against a different Node.js version や NODE_MODULE_VERSION の不一致が出た場合は、better-sqlite3 が 別のNode.jsバージョン向けにビルドされたまま になっている。better-sqlite3 はネイティブアドオンを含むため、Node.jsのバージョンを切り替えた後は再ビルドが必要になることがある。
まず現在のNode.jsバージョンを確認する。
node -vpnpmなら次を実行する。
pnpm rebuild better-sqlite3
pnpm run start:devnpmなら次。
npm rebuild better-sqlite3
npm run start:devそれでも直らない場合は、今使っているNode.jsバージョンで依存関係を入れ直す。
rm -rf node_modules
pnpm install # npmの場合は npm install
pnpm run start:devPrisma 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.tsにdatasource.urlを設定 - NestJSのCommonJS実行に合わせて
generator clientにmoduleFormat = "cjs"を設定 - SQLite用adapter(
@prisma/adapter-better-sqlite3)を使ってPrismaServiceを作成 -
prisma migrate devとprisma generate -
PrismaModule/PrismaServiceとAppModuleへの登録 -
CreateUserDto/UpdateUserDtoの整理 -
UsersServiceのCRUD -
UsersControllerのルーティング -
better-sqlite3のNODE_MODULE_VERSIONエラーが出た場合は、現在のNode.js向けにリビルド -
curlで一覧・作成・取得・更新・削除を確認