Open27

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

takaha4ktakaha4k
  • 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
takaha4ktakaha4k

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.
takaha4ktakaha4k

試しに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リポジトリを作り直すことに

takaha4ktakaha4k

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の扱いがよくわかっていないぞ・・・

takaha4ktakaha4k

npmグローバルインストール

npm i -g @nestjs/cli

バージョンチェック

nest --version            
8.2.5

一旦これで対応。

takaha4ktakaha4k
nest new backend

をすると
Failed to execute command: yarn install --silent
と出た。

一旦フォルダの中身をすべて削除して、再実行する

takaha4ktakaha4k

nest newで自動生成された.gitを削除

cd backend
rm -rf .git
takaha4ktakaha4k

一旦,動作確認

yarn
yarn start

問題なく動く。

takaha4ktakaha4k

usersをつくる。

nest g resource users

Testする

yarn test

OK

takaha4ktakaha4k

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

yarn add -D prisma 
yarn prisma init
takaha4ktakaha4k

結局インポートできんやないかい。

Cannot find module 'nestjs-prisma' or its corresponding type declarations.

    3 import { PrismaService } from 'nestjs-prisma';
                                    ~~~~~~~~~~~~~~~

公式ドキュメント読み直す。

takaha4ktakaha4k

公式ドキュメント通りにローカルSQLiteで試す。

shell
yarn add prisma --dev
npx prisma init
prisma/shema.prisma
// 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?
}
.env
DATABASE_URL="file:./dev.db"

なんか出来たぽいぞ。

takaha4ktakaha4k

あまりprismaを理解できていないので、QuickStartを試す。

shell
curl -L https://pris.ly/quickstart | tar -xz --strip=2 quickstart-main/typescript/starter
cd starter
npm install
script.ts
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()
  })
shell
npm run dev
takaha4ktakaha4k

なるほど。
以下の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
      }
    ]
  }
]
takaha4ktakaha4k

Github Actionsのファイルを作成

mkdir -p .github/workflows && cat > .github/workflows/ci.yaml
takaha4ktakaha4k

SQLiteからMySQLに変更する。

prisma/schema.prisma
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?
}
.env.test
DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests"
takaha4ktakaha4k

Dockerコンテナも用意する

docker-compose.yml
# 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
takaha4ktakaha4k

prismaのソースを追加する

shell
nest g service prisma --no-spec
src/prisma.service.ts
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();
    });
  }
}
takaha4ktakaha4k

src直下にサービスクラスを作る

shell
touch src/user.service.ts 

NestJS公式ではcats.service.tsなどと複数形+サービスと言う命名だった。
しかし、prismaのチュートリアルでは、userと単数形なのが、微妙に気持ち悪いが…

src/user.service.ts
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,
    });
  }
}

takaha4ktakaha4k

同様にpostというサービスクラスを作る

shell
touch src/post.service.ts 
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,
    });
  }
}

takaha4ktakaha4k

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