🏕️

NestJS+prismaでGraphQLを実装する

2022/07/31に公開

個人開発をしているアプリでフロントエンドはNext.jsを使っています。

バックエンドからのデータはGraphQLで実装する必要がありました。
(後々の個人開発でやりたいこともGraphQLで実装しておいたほうが都合が良いと考えました。)

今回はGraphQLをNestJSを使います。その方法を紹介します。

前提

開発環境の主なパッケージなどのバージョンです。
今回はdockerでAPIのコンテナ、DB(postgres)のコンテナを用意しています。
バックエンドのORMとしてPrismaを利用しています。

パッケージ、ミドルウェア バージョン 用途・対象
nestjs 12.1.4(package.jsonより) GraphQLを実行するため
apollo ^3.6.9(package.jsonより)) GraphQLのクライアント
prisma ^4.0.0(package.jsonより) ORMのパッケージ、とりあえず扱いやすそうだったので
postgres 14(docker-compose.ymlより) DBとして利用する
apollo-server-express ^3.10.0(package.jsonより) GraphQLのサーバー

docker-compose.ymlは以下のようにしています。

docker-compose.yml
version: '3'

services:
  app:
    container_name: whaleshark_app
    build:
      context: ./docker/nextjs
      dockerfile: Dockerfile
    volumes:
      - ./Nextjs/app:/usr/src/app
      - ./Nextjs/app/node_modules:/usr/src/app/node_modules
    command: sh -c "npm run dev"
    ports:
      - 4000:3000
      - 6006:6006
    environment:
     - NEXT_PUBLIC_MEASUREMENT_ID=G-XXXXXXXXXX
  api:
    container_name: whaleshark_api
    build:
      context: ./docker/nestjs
      dockerfile: Dockerfile
    command: sh -c "yarn run start:dev"
    ports:
      - 5000:3000
      - 5555:5555
    volumes:
      - ./Nestjs/api:/usr/src/api
      - ./Nestjs/api/node_modules:/usr/src/api/node_modules

  db:
    image: postgres:14
    container_name: whaleshark_db
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      PGDATA: /var/lib/postgresql/data
      POSTGRES_DB: postgres
    volumes:
      - ./db:/var/lib/postgresql/data

https://vercel.com/guides/nextjs-prisma-postgres

https://docs.nestjs.com/graphql/quick-start

https://dev.to/prisma/how-to-setup-a-free-postgresql-database-on-heroku-1dc1

手順

  • NestJSのプロジェクトを作成する
  • DBコンテナを作成する
  • シーディングをする
  • GraphQLをplaygroundから実行する
  • 必要なモデルを追加していく
  • GraphQLをplaygroundから実行する

NestJSのプロジェクトを作成する

まずは公式に沿ってプロジェクトを作成します。

yarn global add @nestjs/cli

nest new <project-name>

必要なパッケージをインストールします。

# For Express and Apollo (default)
$ npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express

# For Fastify and Apollo
# npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-fastify

# For Fastify and Mercurius
# npm i @nestjs/graphql @nestjs/mercurius graphql mercurius@^9

https://docs.nestjs.com/graphql/quick-start

NestJSをインストールするといくつかファイルを生成されます。
その中のapp.module.tsを次のように修正します。

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

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      debug: false,
      playground: true,
      typePaths: ['./**/*.graphql'],
      definitions: {
        path: join(process.cwd(), 'src/graphql.ts'),
        outputAs: 'class',
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

typePathsでGraphQLの(モデルごとの)スキーマの型を記述しておきます。ここではtodo.graphqlのようなファイルを対応するディレクトリに作っておきます。

すると、そのスキーマの記述を見てsrc/graphql.tsに自動的に出力されます

補足:’ts-morph’が見つからないエラーへの対応

'ts-morph'が見つからないというエラーがでた場合は、yarn addすれば解決します。

// このエラーに対しては
Error: Cannot find module 'ts-morph'

// 以下のコマンドで対応する
yarn add ts-morph

DBのコンテナを作成する

次にDBコンテナを作成します。今回はデータベースはpostgresを採用します。

というのもHerokuのpostgresqlを使うためです(無料なので)。

14のstableがherokuであるので、dockerもそれに合わせることにします。

https://www.postgresql.org/about/news/postgresql-14-released-2318/

https://hub.docker.com//postgres](https://hub.docker.com//postgres

docker-compose.ymlはこんな感じでpostgresをprismaが接続するDBとして利用します。

docker-compose.yml
db:
    image: postgres:14
    container_name: whaleshark_db
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      PGDATA: /var/lib/postgresql/data
      POSTGRES_DB: postgres
    volumes:
      - ./db:/var/lib/postgresql/data

docker-compose.ymlに合わせてprismaの.envを編集しておきます。

.env
DATABASE_URL="postgresql://user:password@localhost:5432/postgres"

スキーマを作ります。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

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


model CoffeeShop {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
}

https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate

ここではCoffeeShopというモデルを追加します。新しくマイグレーションファイルを作成します。

// 開発環境におけるマイグレーション
npx prisma migrate dev

// 本番環境におけるマイグレーション
npx prisma migrate

補足:マイグレーションのロールバック

down.sqlというsqlのファイルをdiffを使ってロールバックするようで、Prismaにおけるmigrationのロールバックが微妙にめんどくさいです。

これについてはこれから改良されることを願っています。

https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/generating-down-migrations

シーディングをする

シーダー(Seeder) をつくっていきます。PrismaにおいてはシーディングためのメソッドやAPIがあるわけではないので、自分でコツコツデータを作ってINSERTをするファイルを作成します。

import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();

const coffeeShopData: Prisma.CoffeeShopCreateInput[] = [
  {
    name: 'hoge1',
    email: 'hoge1@example.com',
  },
];

const transfer = async () => {
  const coffeeShops = [];
  for (const c of coffeeShopData) {
    const coffeeShop = prisma.coffee_shop.create({
      data: c,
    });
    coffeeShops.push(coffeeShop);
  }
  return await prisma.$transaction(coffeeShops);
};

const main = async () => {
  await transfer();
};

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

https://www.prisma.io/docs/guides/database/seed-database

補足:prisma studio

prisma studioというツールを使うことで、ブラウザからGUIベースでDBを直接操作することができます。

npx prisma studio

確認:prismaからデータを取得する

prismaから取得できるか確認します。以下のようなコードを書いて実行します。

import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();

const main = async () => {
  const coffeeShops = await prisma.coffeeShop.findMany({
    where: {
      name: 'hoge1',
    },
  });
  console.log(coffeeShops);
};

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

そして、実行するとデータが取得できることが確認できます。

$ npx ts-node  index.ts 

[
  {
    id: 1,
    email: 'hoge1@example.com',
    name: 'hoge1',
    coffeeShopCategoryId: 1
  },
  {
    id: 2,
    email: 'hoge1@example.com',
    name: 'hoge1',
    coffeeShopCategoryId: 1
  }
]

https://www.prisma.io/docs/reference/api-reference/prisma-client-reference

https://zenn.dev/thirosue/books/49a4ee418743ed/viewer/ae5b9a

https://www.prisma.io/docs/concepts/components/prisma-client/crud#read

https://www.prisma.io/docs/concepts/components/prisma-schema/relations/one-to-many-relations

必要なモデルを追加していく

ここからはDBに登録するデータのモデルを作成していきます。手順として以下通りです。

  • モデルを作成する
  • prisma.service.tsを修正する
  • resolverを作成する
  • app.module.tsを修正する

今回はコーヒーショップを登録、取得したいので以下のようなモデルを実装します。
ここは適宜、実装したいモデルを作ります。

src/models/coffeeShop.model.ts
import { Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class CoffeeShop {
  @Field(() => ID)
  id: number;
  name: string;
  email: string;
  url: string;
  price: string;
  imageUrl: string;
}

GraphQLのリゾルバを書きます。

src/coffeeShops/coffeeShops.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PrismaService } from '../prisma.service';
import { CoffeeShop } from './models/coffeeShop.model';

@Resolver(() => CoffeeShop)
export class CoffeeShopsResolver {
  constructor(private prisma: PrismaService) {}

  @Query(() => [CoffeeShop])
  async coffeeShops () {
    return this.prisma.coffeeShop.findMany({});
  }

  @Mutation(() => CoffeeShop)
  async createCoffeeShop(
     @Args('name') name: string,
     @Args('email') email: string,
   ) {
     return this.prisma.coffeeShop.create({ data: { name, email } });
   }
}

prismaのクライアントの設定を書きます。

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();
    });
  }
}

以上で作ったPrismaServiceとCoffeeShopResolverをprovidersに追加します。
autoSchemaFileで自動生成するように設定するのがポイントです。

今回はprovidersにリゾルバを追加しています。コントローラーやファイルの分け方は、好みや実装方針で決めてOKでしょう。

app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { CoffeeShopsResolver } from './coffeeShops/coffeeShops.resolver';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      debug: true,
      playground: true,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
  ],
  controllers: [AppController],
  providers: [AppService, PrismaService, CoffeeShopsResolver],
})
export class AppModule {}

補足:スキーマが反映されないエラー

必要なモデルを追加していると、こんなエラーがvs codeでハイライトされることがあります。

hogehoge does not exist on type 'PrismaService'.

あまり気にすることはないです。テキトーにファイルを操作していると解決されます。

typescriptのビルドがwatch modeで実行されているのと、dockerのボリュームマウントがうまくいってなくて依存関係がうまくなっていないことがあるのでしょう。

テストを書く

GraphQLのテストを書いていきます。NestJSではunitテストとE2Eのテストを書けますけど今回はE2Eのテストを実装します。

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { CoffeeShopsResolver } from './coffeeShops/coffeeShops.resolver';
import { CoffeeCountriesResolver } from './coffeeCountries/coffeeCountries.resolver';
import { CoffeeShopAreasResolver } from './coffeeShopAreas/coffeeShopAreas.resolver';
import console from 'console';

describe('AppController', () => {
  let appController: AppController;
  let coffeeShopsResolver: CoffeeShopsResolver;
  let coffeeCountriesResolver: CoffeeCountriesResolver;
  let coffeeShopAreasResolver: CoffeeShopAreasResolver;
  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [
        AppService,
        PrismaService,
        CoffeeShopsResolver,
      ],
    }).compile();

    appController = app.get<AppController>(AppController);
    coffeeShopsResolver = app.get<CoffeeShopsResolver>(CoffeeShopsResolver);
  });

  // ここはデフォルトの記述
  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });

  describe('coffeeShops findMany()', () => {
    it('should return coffee shops array has keys', async () => {
      const subject = await coffeeShopsResolver.coffeeShops();
      expect('id' in subject[0]).toBe(true);
      expect('name' in subject[0]).toBe(true);
      expect('email' in subject[0]).toBe(true);
    });
  });
});

今回はcoffeeShopResolverのQueryをテストしています。

補足:実際にDBにアクセスしないテストを実装したほうが良い

このE2EのテストだとDBとの接続が必須になってしまい、実行する環境によって結果に差分がでることが考えられます。また実行スピードも毎回DBに接続するため遅くなってしまいます。

できればレスポンスをモックするか、インメモリなテストを実装するのが良いです。

https://docs.nestjs.com/fundamentals/testing#auto-mocking

GraphQLをplaygroundから実行する

実際にGraphQLにリクエストを投げてみます。NestJSではplaygroundが使えるので、そこからクエリを実行します。

リクエストが成功すると、以下のように画面の右半分にレスポンスが返されます。

宣伝

カフェインレスコーヒーを集めたサイトを作っています。
カフェインレスコーヒーは夜に飲んでもぐっすり眠れるので、エンジニアの人にも試してみてほしいな、と思い作りました。

これから色々と機能を追加していく予定です。

https://caffeinelessmore.com/

カラビナテクノロジー デベロッパーブログ

Discussion