NestJS+prismaでGraphQLを実装する
個人開発をしているアプリでフロントエンドは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は以下のようにしています。
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
手順
- 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
NestJSをインストールするといくつかファイルを生成されます。
その中の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://hub.docker.com//postgres](https://hub.docker.com//postgres
docker-compose.ymlはこんな感じでpostgresをprismaが接続するDBとして利用します。
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を編集しておきます。
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?
}
ここではCoffeeShopというモデルを追加します。新しくマイグレーションファイルを作成します。
// 開発環境におけるマイグレーション
npx prisma migrate dev
// 本番環境におけるマイグレーション
npx prisma migrate
補足:マイグレーションのロールバック
down.sqlというsqlのファイルをdiffを使ってロールバックするようで、Prismaにおけるmigrationのロールバックが微妙にめんどくさいです。
これについてはこれから改良されることを願っています。
シーディングをする
シーダー(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();
});
補足: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
}
]
必要なモデルを追加していく
ここからはDBに登録するデータのモデルを作成していきます。手順として以下通りです。
- モデルを作成する
- prisma.service.tsを修正する
- resolverを作成する
- app.module.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のリゾルバを書きます。
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のクライアントの設定を書きます。
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でしょう。
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に接続するため遅くなってしまいます。
できればレスポンスをモックするか、インメモリなテストを実装するのが良いです。
GraphQLをplaygroundから実行する
実際にGraphQLにリクエストを投げてみます。NestJSではplaygroundが使えるので、そこからクエリを実行します。
リクエストが成功すると、以下のように画面の右半分にレスポンスが返されます。
宣伝
カフェインレスコーヒーを集めたサイトを作っています。
カフェインレスコーヒーは夜に飲んでもぐっすり眠れるので、エンジニアの人にも試してみてほしいな、と思い作りました。
これから色々と機能を追加していく予定です。
Discussion