NestJSとprismaをセットアップしてREST APIを作る

- Gitにリポジトリ追加してローカルにクローンする
- NestJSのアップデートを行い、新規プロジェクトを作成する
npm i -g @nestjs/cli
nest new project-name
# yarnを選択
- prismaをセットアップする
yarn add --dev prisma
# or
# yarn add -D prisma
# --devオプションによりdevDependenciesのみにインストール
yarn prisma init
実行後に以下のメッセージ。.envはあとでGit無視リストに追記する。
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore. Don't forget to exclude .env to not commit any secret.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started

CRUD-generaorを活用して、REST API のサンプルつくる
nest g resource
# CUI対話で以下を入力
? What name would you like to use for this resource (plural, e.g., "users")? users
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
実行後に以下メッセージが表示される
CREATE src/users/users.controller.spec.ts (566 bytes)
CREATE src/users/users.controller.ts (894 bytes)
CREATE src/users/users.module.ts (247 bytes)
CREATE src/users/users.service.spec.ts (453 bytes)
CREATE src/users/users.service.ts (609 bytes)
CREATE src/users/dto/create-user.dto.ts (30 bytes)
CREATE src/users/dto/update-user.dto.ts (169 bytes)
CREATE src/users/entities/user.entity.ts (21 bytes)
UPDATE package.json (2056 bytes)
UPDATE src/app.module.ts (312 bytes)
✔ Packages installed successfully.

試しにusersを呼び出す
yarn start
http://localhost:3000/usersアスセスすると
This action returns all users
が表示されることを確認。

試しにyarn testを呼び出す。
yarn test
そうすると Error発生する。
リポジトリ名をライブラリ名を同じにしているのが怪しい。
src/users/users.service.spec.ts:3:31 - error TS2307: Cannot find module 'nestjs-prisma' or its corresponding type declarations.
3 import { PrismaService } from 'nestjs-prisma';
Gitリポジトリを作り直すことに

nestjsを入れる
yarn global add @nestjs/cli
errorが起きる
Internal Error: poc-backend@workspace:.: This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile
yarnの扱いがよくわかっていないぞ・・・

npmグローバルインストール
npm i -g @nestjs/cli
バージョンチェック
nest --version
8.2.5
一旦これで対応。

nest new backend
をすると
Failed to execute command: yarn install --silent
と出た。
一旦フォルダの中身をすべて削除して、再実行する

nest newで自動生成された.gitを削除
cd backend
rm -rf .git

一旦,動作確認
yarn
yarn start
問題なく動く。

usersをつくる。
nest g resource users
Testする
yarn test
OK

公式ドキュメントを参考にyarnでprismaをセットアップする
yarn add -D prisma
yarn prisma init

結局インポートできんやないかい。
Cannot find module 'nestjs-prisma' or its corresponding type declarations.
3 import { PrismaService } from 'nestjs-prisma';
~~~~~~~~~~~~~~~
公式ドキュメント読み直す。

公式ドキュメント通りにローカルSQLiteで試す。
yarn add prisma --dev
npx prisma init
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
posts Post[]
}
model Post {
id Int @default(autoincrement()) @id
title String
content String?
published Boolean? @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
DATABASE_URL="file:./dev.db"
なんか出来たぽいぞ。

あまりprismaを理解できていないので、QuickStartを試す。
curl -L https://pris.ly/quickstart | tar -xz --strip=2 quickstart-main/typescript/starter
cd starter
npm install
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// A `main` function so that you can use async/await
async function main() {
const allUsers = await prisma.user.findMany({
include: { posts: true },
})
// use `console.dir` to print nested objects
console.dir(allUsers, { depth: null })
}
main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
npm run dev

なるほど。
以下のJsonがレスポンスされる。
[
{ id: 1, email: 'sarah@prisma.io', name: 'Sarah', posts: [] },
{
id: 2,
email: 'maria@prisma.io',
name: 'Maria',
posts: [
{
id: 1,
title: 'Hello World',
content: null,
published: false,
authorId: 2
}
]
}
]

Github Actionsのファイルを作成
mkdir -p .github/workflows && cat > .github/workflows/ci.yaml

SQLiteからMySQLに変更する。
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
posts Post[]
}
model Post {
id Int @default(autoincrement()) @id
title String
content String?
published Boolean? @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests"

Dockerコンテナも用意する
# Set the version of docker compose to use
version: '3.9'
# The containers that compose the project
services:
db:
image: postgres:13
restart: always
container_name: integration-tests-prisma
ports:
- '5433:5432'
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests

prismaのソースを追加する
nest g service prisma --no-spec
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

以前作ったUsersServiceクラスを削除
rm -rf src/users

src直下にサービスクラスを作る
touch src/user.service.ts
NestJS公式ではcats.service.tsなどと複数形+サービスと言う命名だった。
しかし、prismaのチュートリアルでは、userと単数形なのが、微妙に気持ち悪いが…
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from '@prisma/client';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
): Promise<User | null> {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({
data,
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
return this.prisma.user.update({
data,
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
return this.prisma.user.delete({
where,
});
}
}

同様にpostというサービスクラスを作る
touch src/post.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Post, Prisma } from '@prisma/client';
@Injectable()
export class PostService {
constructor(private prisma: PrismaService) {}
async post(
postWhereUniqueInput: Prisma.PostWhereUniqueInput,
): Promise<Post | null> {
return this.prisma.post.findUnique({
where: postWhereUniqueInput,
});
}
async posts(params: {
skip?: number;
take?: number;
cursor?: Prisma.PostWhereUniqueInput;
where?: Prisma.PostWhereInput;
orderBy?: Prisma.PostOrderByWithRelationInput;
}): Promise<Post[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.post.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
return this.prisma.post.create({
data,
});
}
async updatePost(params: {
where: Prisma.PostWhereUniqueInput;
data: Prisma.PostUpdateInput;
}): Promise<Post> {
const { data, where } = params;
return this.prisma.post.update({
data,
where,
});
}
async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
return this.prisma.post.delete({
where,
});
}
}

既存のappクラス郡を修正。
しかし、環境変数周りが怪しいため、yarn startできない