はじめに
TypeScript + Prisma + NestJSでGraphQLサーバを作っていきます。
1.NestJSプロジェクトの作成
npm install -g @nestjs/cli
nest new my-app
インストール後、サーバを起動してみます。
cd my-app
npm run start
http://localhost:3000
にアクセスすると Hello World! が表示されます。
2.Prismaのセットアップ
npm install prisma --save-dev //Prismaのパッケージをインストール
npm install @prisma/client
npx prisma init //prismaの初期化
prisma/schema.prisma
と .envファイル
が生成されるので接続したいデータベースに合わせて設定していきます。
今回は、supabaseを使用します。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"
3.テーブルの作成
今回はIDと本文とのみの Memo テーブルを作成します。
model Memo {
id Int @id @default(autoincrement())
content String
}
以下のコマンドを実行し、マイグレーションファイルの生成とマイグレーションの実行します。
npx prisma migrate dev --name memo
4.コマンドを使用してファイルを作成
nest g resource memo
コマンドを入力すると質問されるので、GraphQLのcode firstを選択します。
What transport layer do you use? GraphQL (code first)
Would you like to generate CRUD entry points? Yes
コードファーストとスキーマファースト
NestJS で GraphQL サーバーを開発の開発には、コードファーストとスキーマファーストのアプローチがあります。どちらを採用しても、「Schema、データ、Resolver を用意すれば GraphQL サーバーは動かせる」という原理に変わりありません。この選択は、Schema をどうやって作成するか という違いです。今回はコードファーストのアプローチを取ります。
コードファーストのアプローチでは、TypeScriptのクラスとデコレータを使用してクラス定義からGraphQLのSDLを生成します。
下記フォルダ、ファイルが作成され、app.module.tsは更新されます。
CREATE src/memo/memo.module.ts (218 bytes)
CREATE src/memo/memo.resolver.spec.ts (515 bytes)
CREATE src/memo/memo.resolver.ts (1098 bytes)
CREATE src/memo/memo.service.spec.ts (446 bytes)
CREATE src/memo/memo.service.ts (623 bytes)
CREATE src/memo/dto/create-memo.input.ts (196 bytes)
CREATE src/memo/dto/update-memo.input.ts (243 bytes)
CREATE src/memo/entities/memo.entity.ts (187 bytes)
UPDATE src/app.module.ts (308 bytes)
5.NestJSでPrismaクライアントを使用する
NestJSでPrismaClientを使用するために、src 配下に prisma.service.ts というファイルを作成し、PrismaClientのインスタンス化とデータベースへ接続します。
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
これでPrismaServiceからDB操作のためのメソッドを呼び出すことができるようになりました。
6.GraphQLのパッケージをインストール
NestJSの GraphQLのページを参考に必要なパッケージをインストールします。
npm install @nestjs/graphql @nestjs/apollo graphql apollo-server-express
7.app.module.tsの編集
graphqlモジュールをNestJSアプリケーションに組み込み初期化します。
AppModule に GraphQLModule と先程作成した PrismaServiceを追加します。
import { ApolloDriver } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MemoModule } from './memo/memo.module';
import { PrismaService } from './prisma.service';
@Module({
imports: [
GraphQLModule.forRoot({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
MemoModule,
],
controllers: [AppController],
providers: [AppService, PrismaService],
})
export class AppModule {}
GraphQLModuleのforRoot()
には GraphQL のオプションを設定できます。
autoSchemaFile: join(process.cwd(), 'src/schema.gql')
は autoSchemaFileプロパティ
によって、プロジェクトのsrcディレクトリに自動的にGraphQLスキーマファイルが生成されるようにしています。
これでgraphqlが使用できるようになりました!
8.テーブルの各データをモデルとして定義
編集前
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class Memo {
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
}
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType()
export class Memo {
@Field(() => ID, { description: 'メモのID' })
id: number;
@Field({ description: 'メモの内容' })
content: string;
}
description には項目や補足を書くなど便利に使用できます。自動で生成されるschema.gqlにも反映されます。
9.サービス定義
編集前
import { Injectable } from '@nestjs/common';
import { CreateMemoInput } from './dto/create-memo.input';
import { UpdateMemoInput } from './dto/update-memo.input';
@Injectable()
export class MemoService {
create(createMemoInput: CreateMemoInput) {
return 'This action adds a new memo';
}
findAll() {
return `This action returns all memo`;
}
findOne(id: number) {
return `This action returns a #${id} memo`;
}
update(id: number, updateMemoInput: UpdateMemoInput) {
return `This action updates a #${id} memo`;
}
remove(id: number) {
return `This action removes a #${id} memo`;
}
}
This action~のところを編集していきましょう。
import { Injectable } from '@nestjs/common';
import { CreateMemoInput } from './dto/create-memo.input';
import { UpdateMemoInput } from './dto/update-memo.input';
import { PrismaService } from 'src/prisma.service';
@Injectable()
export class MemoService {
constructor(private prisma: PrismaService) {}
async create(createMemoInput: CreateMemoInput) {
return this.prisma.memo.create({ data: createMemoInput });
}
findAll() {
return this.prisma.memo.findMany();
}
findOne(id: number) {
return this.prisma.memo.findUnique({ where: { id } });
}
update(id: number, updateMemoInput: UpdateMemoInput) {
return this.prisma.memo.update({
where: { id: id },
data: updateMemoInput,
});
}
remove(id: number) {
return this.prisma.memo.delete({ where: { id: id } });
}
}
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class CreateMemoInput {
@Field({ description: '記事の追加' })
content: string;
}
import { CreateMemoInput } from './create-memo.input';
import { InputType, Field, PartialType, ID } from '@nestjs/graphql';
@InputType()
export class UpdateMemoInput extends PartialType(CreateMemoInput) {
@Field(() => ID, { description: '更新するID' })
id: number;
@Field({ description: '更新の内容' })
content: string;
}
number はデフォルトではGraphQLの Float に変換されるため、ここでは ID になるよう明示的に @Field(() => ID) を指定しています。
memo.module.tsにPrismaServiceを追加。
import { Module } from '@nestjs/common';
import { MemoService } from './memo.service';
import { MemoResolver } from './memo.resolver';
import { PrismaService } from 'src/prisma.service';
@Module({
providers: [MemoResolver, MemoService, PrismaService],
})
export class MemoModule {}
import { Resolver, Query, Mutation, Args, Int, ID } from '@nestjs/graphql';
import { MemoService } from './memo.service';
import { Memo } from './entities/memo.entity';
import { CreateMemoInput } from './dto/create-memo.input';
import { UpdateMemoInput } from './dto/update-memo.input';
@Resolver(() => Memo)
export class MemoResolver {
constructor(private readonly memoService: MemoService) {}
@Mutation(() => Memo, { description: '記事の作成' })
createMemo(@Args('createMemoInput') createMemoInput: CreateMemoInput) {
return this.memoService.create(createMemoInput);
}
@Query(() => [Memo], { description: '記事をすべて取得する' })
findAll() {
return this.memoService.findAll();
}
@Query(() => Memo, { description: '特定の記事の取得' })
findOne(@Args('id', { type: () => ID }) id: number) {
return this.memoService.findOne(id);
}
@Mutation(() => Memo, { description: '記事の更新' })
updateMemo(@Args('updateMemoInput') updateMemoInput: UpdateMemoInput) {
return this.memoService.update(updateMemoInput.id, updateMemoInput);
}
@Mutation(() => Memo, { description: '記事の削除' })
removeMemo(@Args('id', { type: () => ID }) id: number) {
return this.memoService.remove(id);
}
}
resolverはコマンドを打った時にほとんどコードが書かれています。 descriptionの追加をしました。
これで、完了です。サーバーを立ち上げましょう。
npm run start
10.動作確認
http://localhost:3000/graphql にアクセスするとGraphQL Playgroundが表示されます。
mutation {
createMemo(createMemoInput: { content: "aiu" }) {
content
}
}
query {
findAll {
id
content
}
}
mutation {
updateMemo(updateMemoInput: { id: 2, content: "記事の更新" }) {
id
content
}
}
mutation {
removeMemo(id: 2) {
id
content
}
}
query {
findOne(id: 1) {
id
content
}
}