TypeScript + Prisma + NestJSでGraphQLサーバを作ってみる
はじめに
本記事では、TypeScriptとPrismaとNestJSを使ってGraphQLサーバを作ってみます。
最終的には、以下のような記事の取得と作成ができるGraphQLサーバができあがります。
Prismaとは
Prismaは、以下の3つのツールで構成されたNode.jsとTypeScriptのためのORMです。
- Prisma Client: 自動生成される型安全なデータベースクライアント
- Prisma Migrate: 宣言的なデータモデリングとカスタマイズ可能なマイグレーション
- Prisma Studio: データを閲覧・編集するためのモダンなGUI
開発体験が良く、先日のJSConf JPの @qsonaさんの発表 でもあったように最近では新規サービスでの採用事例が増えてきました。
NestJSとは
NestJSは、素早くスケーラブルなサーバーサイドアプリケーションを構築するためのNode.jsフレームワークです。
実用的なアーキテクチャをすぐに構築できることを目的としていて、疎結合で、テストや保守しやすく、スケーラブルなアプリケーションを作成できます。
HTTPサーバとしての実装はデフォルトではExpressを利用しており、オプションでFastifyに変更することもできます。
また、TypeScriptをデフォルトでサポートしています。
最近では メルカリShops でも採用され、徐々に国内でも採用事例を聞くようになりました。
GraphQLサーバを作ってみる
それでは、早速GraphQLサーバを作っていきましょう!
本記事で使用するNestJSとPrismaのバージョンは以下の通りです。
Nest CLI: 8.2.1
NestJS: 8.3.1
Prisma: 3.10.0
1. NestJSプロジェクトの作成
NestJSのPrismaのドキュメント を参考に、NestJSとPrismaのセットアップをしていきます。
まずはNest CLIをインストールし、NestJSのプロジェクトを作成します。
$ npm install -g @nestjs/cli
$ nest new my-app
ここで生成されるファイルの詳細を知りたい方は NestJSのドキュメント をご覧ください。
それでは、試しにサーバを起動してみましょう。
$ cd my-app
$ npm run start
http://localhost:3000 にアクセスすると Hello World!
が表示されます。
2. Prismaのセットアップ
続いて、Prismaのセットアップを行います。
$ npm install prisma --save-dev
$ npm install @prisma/client
$ npx prisma init
このコマンドで prisma/schema.prisma
と .env
ファイルが生成されるので、それらを接続したいデータベースに合わせて編集します。
デフォルトではPostgreSQLを使うようになっていますが、今回はMySQLを使うため、db
の provider
を mysql
に変更し、DATABASE_URL
もMySQLのURLに書き換えます。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/DATABASE"
# ex: DATABASE_URL="mysql://root:password@localhost:3306/my_app"
3. テーブルの作成
今回は記事メディアのようなサービスを想定して、タイトルと本文と公開フラグのカラムを持った Post
テーブルを作成します。
Post
モデルの定義を先程の schema.prisma
に追加します。
model Post {
id Int @default(autoincrement()) @id
title String
content String
published Boolean @default(false)
}
以下のコマンドを実行すると、マイグレーションファイルの生成とマイグレーションの実行が行われます。
$ npx prisma migrate dev --name post
prisma
ディレクトリには以下のようなマイグレーションファイルが生成されました。
$ tree prisma
prisma
├── migrations
│ ├── 20220223034555_post
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
生成されたマイグレーションファイルの中身
-- CreateTable
CREATE TABLE `Post` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(191) NOT NULL,
`content` VARCHAR(191) NOT NULL,
`published` BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Prismaについてさらに詳しく知りたい方は Prismaのドキュメント を参照してみてください。
4. Prisma Studioでデータを追加してみる
PrismaにはPrisma Studioというデータベースに対してCRUDの操作ができるGUIが付いています。
以下のコマンドでPrisma Studioを起動して、レコードを追加してみましょう。
$ npx prisma studio
http://localhost:5555 にアクセスして Post
テーブルにレコードを追加してみてください。
5. NestJSでPrismaクライアントを使用する
NestJSでPrismaクライアントを使用するために、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();
});
}
}
6. GraphQLのパッケージをインストール
ここからGraphQLの実装に入っていきます。
NestJSのGraphQLのページ を参考に必要なパッケージをインストールします。
$ npm install @nestjs/graphql @nestjs/apollo graphql apollo-server-express
7. NestJSのGraphQLプラグインを有効化
NestJSのGraphQLプラグインを有効化すると必要な定型コードの量を減らすことができるので、nest-cli.json
に compilerOptions
を追加して有効化しておきます。
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/graphql"]
}
}
詳しくは https://docs.nestjs.com/graphql/cli-plugin に記載されています。
8. Object Typeの作成
GraphQLではコードファーストとスキーマファーストのアプローチがありますが、今回はコードファーストのアプローチを取ります。
コードファーストのアプローチでは、TypeScriptのデコレータを使用してクラス定義からGraphQLのSDLを生成します。
Post
のObject Typeを作成してみましょう。
Post
クラスを作り、@ObjectType()
のデコレータを付けます。
import { Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field(() => ID)
id: number;
title: string;
content: string;
published: boolean;
}
GraphQLプラグインを有効化していない場合は各プロパティに @Field()
を付ける必要がありますが、有効化している場合は各プロパティの情報から自動で判断してスキーマを生成してくれます。
上記のコードから以下のようなGraphQLのスキーマが生成されます。
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
}
9. Resolverの作成
続いて、GraphQLでデータ操作を行うためのResolverを作成していきます。
今回は、Post
の一覧を取得するQueryとレコードを新規作成するMutationを作成します。
Resolverを作るには、Resolverクラスを作成し、@Resolver()
のデコレータを付けます。
そして、QueryとMutationの関数には @Query()
と @Mutation()
、引数には @Args()
を付けます。
これらの情報からGraphQLのスキーマが生成されます。
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PrismaService } from 'src/prisma.service';
import { Post } from './models/post.model';
@Resolver(() => Post)
export class PostsResolver {
constructor(private prisma: PrismaService) {}
@Query(() => [Post])
async posts() {
return this.prisma.post.findMany();
}
@Mutation(() => Post)
async createPost(
@Args('title') title: string,
@Args('content') content: string,
) {
return this.prisma.post.create({ data: { title, content } });
}
}
QueryやMutationの関数の中では、Prisma Clientを使ってレコードの取得や作成を行っています。
10. AppModuleに必要なモジュールとクラスを追加
最後に、AppModule
に GraphQLModule
と先程作成した PrismaService
、 PostsResolver
を追加します。
@Module({
- imports: [],
+ imports: [
+ GraphQLModule.forRoot<ApolloDriverConfig>({
+ driver: ApolloDriver,
+ autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
+ }),
+ ],
controllers: [AppController],
- providers: [AppService],
+ providers: [AppService, PrismaService, PostsResolver],
})
export class AppModule {}
動作確認
これでGraphQLサーバが完成したので動作確認をしてみましょう。
以下のコマンドでサーバを起動します。
$ npm run start
http://localhost:3000/graphql にアクセスするとGraphQL Playgroundが表示されます。
Queryで記事の一覧を取得してみます。
query {
posts {
id
title
content
published
}
}
4.で作成した Post
の一覧が取得できます。
続いて、Mutationで新しい記事を作成してみます。
mutation {
createPost(title: "piyo", content: "3記事目の本文です。") {
id
title
content
published
}
}
新しいレコードが追加されたことが確認できます。
おめでとうございます🥳
TypeScript + Prisma + NestJSでGraphQLサーバを作ることができました!
まとめ
本記事では、TypeScriptとPrismaとNestJSを使ってGraphQLサーバを作ってみました。
PrismaとNestJSのおかげで、あっという間にデータの取得と作成のできるGraphQLサーバを作ることができました。
記事が長くなりすぎるので今回は説明しませんでしたが、GraphQLでは1つのQueryで関連テーブルまで取得する際にN+1問題が発生しがちです。
それを防ぐための仕組みがPrismaにはデフォルトで用意されていてとても便利なので、近いうちにその辺りも記事にまとめたいと思います。
Discussion