🙈

【入門】NestJS+Prisma+prisma-nestjs-graphqlで快適なGraphQL環境構築🧑🏽‍💻

2022/03/24に公開

はじめに

NestJSについて

NestJSはGitHubのスター数を勢いよく伸ばしており、今後も伸びていくことが予想される注目のNodeJSフレームワークです。あくまで参考程度ですが、NestJSとNestJS以外のよく使われるバックエンドのフレームワークのGitHubのスター数の推移の比較をしてみました。こうやって比較してみると、どのフレームワークも人気なものの、今後の伸びの勢いという点ではDjango、FastAPI、gin、NestJSあたりに軍杯があがる気がします。(特にFastAPIの勢いは凄いですね。)

Prismaについて

PrismaはTypeScriptのORMです。TypeScriptのORMというと、今まではTypeORMが使われることが多かったようですが、最近のPrismaの伸びを見る限り、Prismaが主流になるのは時間の問題(もうなっている?)の様な気がします。

Prismaは既に業務で使っているものの、TypeORMは個人開発で多少触った程度なので、あまり詳しくは比較できないのですが、使いやすさはやはりPrismaの方が上だと感じています。また、使いやすいだけでなく、generatorを使って至れり尽くせりな様々な機能を追加できる点も魅力です。更に、GraphQLを使う上での課題だったN+1問題もクエリの最適化によって解決されていて、GraphQLとの相性も良い点も大きな魅力です。

https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance

GraphQLについて(RESTAPIは使ったことあるけど...という方向けの簡単な説明)

(※お手製の雑な図で申し訳ありません)
GraphQLというと、とっつきづらい印象を持っている方も少なくないかもしれませんが、NestJSで単純にGraphQLサーバーを動かす事だけを考えると、まずは赤字の部分であるResolverとスキーマ定義の書き方さえ覚えてしまえば、動作させることが出来てしまいます!

早速実践してみよう!

淡々と手順を紹介していくので、分からない箇所等ありましたら質問頂けると助かります。

NestCLIでプロジェクトを作成

1.NestCLIをインストールします。

terminal
# 既にインストールしている場合はスキップして大丈夫です。
npm install -g @nestjs/cli

2.NestCLIでプロジェクト作成します。

terminal
# プロジェクト作成、パッケージマネージャーはnpmを選択します。
nest new hello-prisma
# プロジェクトフォルダに移動
cd hello-prisma

DB(MySQL)のセットアップ

1.compose.ymlを作成します。

terminal
# 下のcompose.ymlの中身をコピペしてCRTL+Dで保存します。普通に作成しても大丈夫です。
cat > compose.yml
compose.yml
services:
  db:
    image: mysql:8.0
    ports:
      - 3308:3306
    volumes:
      - db-store:/var/lib/mysql
      - ./mysql_logs:/var/log/mysql
    environment:
      MYSQL_DATABASE: test
      MYSQL_ROOT_PASSWORD: root
volumes:
  db-store:

2.MySQLのコンテナを起動します。

terminal
 # MySQLのコンテナを起動します。
 docker compose up
 # ホスト側からMySQLに接続できるか確認
 mysql -h 127.0.0.1 -u root -P 3308 -proot

ホスト側のポートは3306を避けて、3308にしています。
docker compose v2の仕様に従って書いています。
https://zenn.dev/miroha/articles/whats-docker-compose-v2

Prismaのセットアップ

1. nest add nestjs-prismaを実行

terminal
npm install nestjs-prisma

2. AppModuleにPrismaModuleをimportしてアプリケーション全体でPrismaが使えるようにします

app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [PrismaModule.forRoot({ isGlobal: true }),],
})
export class AppModule {}

テーブルの作成

1. /prisma/schema.prismaを編集します。

schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

//追加
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?
}

2. .envを作成します。

terminal
# 下の.envの内容でファイルを新規作成
 echo DATABASE_URL=\"mysql://root:root@localhost:3308/test\" > .env 

3. schema.prismaの内容をDBに反映し、migrationファイルを作成します。

terminal
# migrationファイルが作成されます。名前を聞かれるので適当に入力します。
npx prisma migrate dev

seedの作成

後にGraphQLでDBからデータを取得したい為、初期データを投入します。

1.package.jsonの以下の項目を編集します。

package.json
"prisma": {
    "seed": "ts-node ./prisma/seed/start.ts"
  }

2.start.tsを作成します。
量が増えてくると、ファイルを分割したほうが見通しが良くなると思いますが、今回は分割せずにstart.tsのみに書きます。

terminal
# seedフォルダを作成
mkdir prisma/seed
#以下のstart.tsをコピペします。
cat > prisma/seed/start.ts
start.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const main = async () => {
  console.log('💫 seed executing ...');
  await prisma.post.deleteMany();
  await prisma.user.deleteMany();
  await prisma.user.create({
    data: {
      name: 'john',
      email: 'john@gmail.com',
      posts: {
        create: {
          title: 'first article',
          content: 'hello!world!',
          published: true,
        },
      },
    },
  });
  console.log('💫 seed finished.');
};

main()
  .catch((e) => console.error(e))
  .finally(async () => {
    await prisma.$disconnect();
  });

3.seedを実行します。

terminal
npx prisma db seed

GraphQLのセットアップ

1.ライブラリをインストールします。

terminal
# ライブラリのインストール
npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express

2.app.module.tsを書き換えます。

app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from 'nestjs-prisma';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      debug: true,
      playground: true,
      autoSchemaFile: './src/schema.graphql',
    }),
    PrismaModule.forRoot({ isGlobal: true }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

prismaにジェネレーター(prisma-nestjs-graphql)を追加する

1.ライブラリをインストールします。

terminal
# prisma-nestjs-graphqlをインストール
npm install prisma-nestjs-graphql

2.schema.prismaにgeneratorを追加します。

schema.prisma
//追加
generator nestgraphql {
    provider = "prisma-nestjs-graphql"
    output = "../src/@generated/prisma-nestjs-graphql"
}

これでnpx prisma generateの実行によって、コードファーストアプローチのGraphQLのコードが生成されるようになります。早速実行したいところですが、かなりの数のファイルが自動生成される為、自動生成されるファイルはgitの管理外にします。

3. .gitignoreにprisma-nestjs-graphqlで生成されるファイルを追加します・

terminal
echo "src/@generated" >> .gitignore

4. npx prisma generateを実行します。

terminal
npx prisma generate

src/@generatedにGraphQLのコードが生成されたことが確認できると思います。

NestCLIでテンプレートを生成

同じ様なコードを何回も書くのは面倒な為、NestCLIを利用します。

1. nest g resource でmoduleを作成します。

terminal
# transport layerを聞かれたら GraphQL (code first) を選択してください
# CRUD entry pointsを生成するかどうか聞かれたら yes を選択してください
nest g resource modules/users

このコマンドによってmodulesフォルダの中にuserフォルダが作成されていることが確認できると思いますが、この段階ではsrcフォルダの中にschema.graphql(SDL)はまだ生成されていないと思います。NestJSサーバーを起動することによって、AppMoudleの依存関係が解決され、schema.graphqlが生成されます。

2.サーバーを起動します。

terminal
npm run start:dev

これでsrcフォルダの中にschema.graphqlが生成されたと思います。

schema.graphqlとデコレータの対応を確認してみる

無事にschema.graphqlが生成され、中身を確認すると以下のようになっていると思います。

schema.graphql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

type User {
  """Example field (placeholder)"""
  exampleField: Int!
}

type Query {
  users: [User!]!
  user(id: Int!): User!
}

type Mutation {
  createUser(createUserInput: CreateUserInput!): User!
  updateUser(updateUserInput: UpdateUserInput!): User!
  removeUser(id: Int!): User!
}

input CreateUserInput {
  """Example field (placeholder)"""
  exampleField: Int!
}

input UpdateUserInput {
  """Example field (placeholder)"""
  exampleField: Int
  id: Int!
}

冒頭でResolverとSchema定義の書き方さえ覚えてしまえば、動作させることが出来ると述べましたが、コードファーストアプローチの場合、デコレータでSDL(Schema Definition Language)を生成する為、どのデコレータがどのようなSDLを生成しているのかあまり意識しなくても実装できてしまうのですが、今回は折角なので(?)少しだけ確認していきたいと思います。

@ObjectType

schema.graphql
type User {
  """Example field (placeholder)"""
  exampleField: Int!
}

上記のコードは以下のsrc/modules/users/entities/users.entity.tsのデコレータから生成されています。

user.entity.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => Int, { description: 'Example field (placeholder)' })
  exampleField: number;
}

NestJSでコードファーストアプローチでGraphQLを実装する場合は、@ObjectTypeを使って、 データモデリングを行います。(場合によってはDBのテーブル構造をそのまま反映するのではなく、クライアント側で使いやすいようにデータモデリングするのが良いアプローチとされています。)

@Query

schema.graphql
type Query {
  users: [User!]!
  user(id: Int!): User!
}

上記のコードは以下のsrc/modules/users/entities/users.resolver.tsのデコレータから生成されています。

users.resolver.ts
  @Query(() => [User], { name: 'users' })
  findAll() {
    return this.usersService.findAll();
  }

  @Query(() => User, { name: 'user' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.usersService.findOne(id);
  }

デコレータの第1引数には関数を渡し、リゾルバーが返す型を関数の返り値で表現しています。
デコレータの第二引数に{ name: 'user' }を渡すことによって、Queryのフィールド名を変更することが出来ます。もし、このオブジェクトを渡さなかった場合、フィールド名はメソッド名となります。

@Mutation

schema.graphql
type Mutation {
  createUser(createUserInput: CreateUserInput!): User!
  updateUser(updateUserInput: UpdateUserInput!): User!
  removeUser(id: Int!): User!
}

上記のコードは以下のsrc/modules/users/entities/users.resolver.tsのデコレータから生成されています。

users.resolver.ts
  @Mutation(() => User)
  updateUser(@Args('updateUserInput') updateUserInput: UpdateUserInput) {
    return this.usersService.update(updateUserInput.id, updateUserInput);
  }

  @Mutation(() => User)
  removeUser(@Args('id', { type: () => Int }) id: number) {
    return this.usersService.remove(id);
  }

QueryとMutationの使い分けとしては、サーバーからデータを取得するにはQueryを使い、DBへのINSERT、UPDATE,DELETEを行う場合はMutationを使います。技術的な違いとしては、GraphQLでは一度のリクエストに複数のQueryやMutationを含めることが出来るのですが、Queryが複数含まれていた場合、複数のQueryが並列で実行されるのに対して、Mutationが複数含まれていた場合、複数のMutationが順番に実行されるということです。途中のMutationが失敗したらそれ以降のMutationはキャンセルされますが、それ以前に実行していたMutationはロールバックされません。その為、例えば三つのMutationを一度のリクエストに含めて実行する場合、一つだけ成功する場合や2つだけ成功する場合が許容できない時は、その三つのMutationを一つのMutationにまとめるべきだと言えます。

Resolverの実装

ここまで少し長かったですが、遂にResolverを実装していきたいと思います!

1. src/@generatedの中から使えるファイルをコピーします。

terminal
# @generatedはgitの管理外になっている為、必要なファイルだけコピーします。
# postsモジュールを作成
nest g resource modules/posts
# ファイルの中身をコピー
cat src/@generated/prisma-nestjs-graphql/user/user.model.ts > src/modules/users/entities/user.entity.ts
cat src/@generated/prisma-nestjs-graphql/user/user-count.output.ts > src/modules/users/entities/user-count.output.ts
cat src/@generated/prisma-nestjs-graphql/post/post.model.ts > src/modules/posts/entities/post.entity.ts

2. import文を書き換えます。
src/modules/posts/entities/post.entity.ts

post.entity.ts
import { Field } from '@nestjs/graphql';
import { ObjectType } from '@nestjs/graphql';
import { ID } from '@nestjs/graphql';
//変更
import { User } from '../../users/entities/user.entity';
import { Int } from '@nestjs/graphql';

////////////////略/////////////////////////

src/modules/user/entities/user.entity.ts

user.entity.ts
import { Field } from '@nestjs/graphql';
import { ObjectType } from '@nestjs/graphql';
import { ID } from '@nestjs/graphql';
//変更
import { Post } from '../../posts/entities/post.entity';
//追加
import { UserCount } from './user-count.output';

////////////////略/////////////////////////

3. users.service.tsを編集します。

users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { PrismaService } from 'nestjs-prisma';

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

  create(createUserInput: CreateUserInput) {
    return 'This action adds a new user';
  }

  findAll() {
    return this.prisma.user.findMany({
      include: {
        _count: {},
        posts: {},
      },
    });
  }

  findOne(id: number) {
    return `This action returns a #${id} user`;
  }

  update(id: number, updateUserInput: UpdateUserInput) {
    return `This action updates a #${id} user`;
  }

  remove(id: number) {
    return `This action removes a #${id} user`;
  }
}

GraphQLPlaygroundでデータを取得できるか確認します。

お疲れ様です!ここまで上手くいっていればプレイグラウンドでGraphQLの動作を確認することが出来るはずです!

1. サーバーを起動します。

terminal
npm run start:dev

2. localhost:3000/graphqlにアクセスして以下のクエリーを実行します

query{
  users{
   name
    id
    email
    name
    _count{
      posts
    }
    posts{
      content
      title
      published
      authorId
    }
  }
}

成功すれば以下の様になると思います🎉

まとめ

ここまで駆け足でNestJS+PrismaでのGraphQL実装を解説していきましたが、実際にはMiddleWareやGuardで認証関係の処理をしたり、本番運用の為にprismaのログを吐き出す為の設定をしたり、パフォーマンスを重視する場合にはHTTPプロバイダをexpressからfastifyに変更したりなどなど、まだまだやる事がたくさんあると思います。自分には以下のレポジトリがとても参考になった為、紹介させて頂きます。
最後に、この記事に関して間違いの指摘や質問等ありましたらコメントして頂けると嬉しいです!
ここまで読んで頂き、ありがとうございました。

https://github.com/notiz-dev/nestjs-prisma-starter/tree/main/src

Discussion