tag:crieit.net,2005:https://crieit.net/tags/NestJS/feed
「NestJS」の記事 - Crieit
Crieitでタグ「NestJS」に投稿された最近の記事
2022-08-31T19:41:17+09:00
https://crieit.net/tags/NestJS/feed
tag:crieit.net,2005:PublicArticle/18289
2022-08-31T18:48:03+09:00
2022-08-31T19:41:17+09:00
https://crieit.net/posts/NestJS-Prisma-PlanetScale-REST-API-GraphQL
NestJSとPrismaとPlanetScaleでREST APIとGraphQLサーバを作る
<h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1>
<p>Node.jsをベースとしたAPIを作ろうとしたときに、REST APIとGraphQLを同時に生やしたいと思ったので、その流れについて記事にしようと思います。</p>
<h2 id="この記事の目標"><a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E7%9B%AE%E6%A8%99">この記事の目標</a></h2>
<p>NestJSでREST APIとGraphQLが同時に動くサーバを作成する</p>
<h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2>
<ul>
<li>データベース:PlanetScale</li>
<li>ORM:Prisma</li>
<li>フレームワーク:NestJS</li>
</ul>
<h1 id="REST APIの実装"><a href="#REST+API%E3%81%AE%E5%AE%9F%E8%A3%85">REST APIの実装</a></h1>
<p>最初にREST APIを作成し、その次にGraphQLを作成します。</p>
<h2 id="NestJSでプロジェクトを作成"><a href="#NestJS%E3%81%A7%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90">NestJSでプロジェクトを作成</a></h2>
<p>Prismaの利用を前提としたNestJSの公式チュートリアルがあるので、やっておくと理解しやすいですが、</p>
<p><a target="_blank" rel="nofollow noopener" href="https://docs.nestjs.com/recipes/prisma">https://docs.nestjs.com/recipes/prisma</a></p>
<p>やや説明不足&実装不足でこのままだと動かないので、以下の記事も参考にすると良いでしょう。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59">https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59</a></p>
<p>まず、NestJS CLIを使ってプロジェクトを作成します。<br />
わざわざグローバルインストールする必要はないのでnpxで作ります。</p>
<pre><code class="sh">npx @nestjs/cli new プロジェクト名
</code></pre>
<p>するとプロジェクト名に設定した名前のフォルダが生成されます。<br />
シェル上で、使用したいパケージマネージャを選択するプロンプトが表示されるので、好きなものを選択してください。今回はyarnを選びます。</p>
<pre><code class="sh">? Which package manager would you ❤️ to use? (Use arrow keys)
npm
> yarn
pnpm
</code></pre>
<p>選ぶとインストールがいずれ完了します。<br />
完了したら、</p>
<pre><code class="sh">cd プロジェクト名
yarn start
</code></pre>
<p>でNestJSが起動することを確認してみましょう。<br />
<a target="_blank" rel="nofollow noopener" href="http://localhost:3000">http://localhost:3000</a> にアクセスすると、<code>Hello World!</code>と表示されているはずです。</p>
<p>ちなみに、</p>
<pre><code class="sh">yarn start:dev
</code></pre>
<p>とすると変更監視モードで起動できます。コードの変更をすぐに確認したいときは、こちらで起動し続けると便利です。</p>
<h2 id="GitHubにリポジトリを作成"><a href="#GitHub%E3%81%AB%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90">GitHubにリポジトリを作成</a></h2>
<p>ここまで来たら、GitHubに接続しましょう。<br />
package.jsonの内容を自分に合わせて書き換えて、GitHubにPublishしました。</p>
<h2 id="Prismaを導入する"><a href="#Prisma%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B">Prismaを導入する</a></h2>
<p>Prisma CLIを開発環境にインストールします。</p>
<pre><code class="sh">yarn add -D prisma
</code></pre>
<p>Prisma Clientをインストールします。</p>
<pre><code class="sh">yarn add @prisma/client
</code></pre>
<p>Prismaを初期化します。</p>
<pre><code class="sh">yarn prisma init
</code></pre>
<p>.envファイルが作られますが、現時点で.gitignoreに<code>.env</code>が指定されていません。<br />
セキュリティ上問題があるので、<code>.env</code>を追加してください。</p>
<pre><code class="gitignore">.env
</code></pre>
<p>ここまで終わったら、データベースを接続していきます。</p>
<h2 id="PlanetScaleを用意"><a href="#PlanetScale%E3%82%92%E7%94%A8%E6%84%8F">PlanetScaleを用意</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tak001/items/cfbaa9dcb542929ff235">https://qiita.com/tak001/items/cfbaa9dcb542929ff235</a></p>
<p>この辺の記事を参考にして、PlanetScaleプロジェクトを作成してください。<br />
作成できたら、Branchesタブから<code>main</code>ブランチを選択し、Connectボタンを押して、接続情報を表示してください。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/682f9e8b-6459-2283-10d1-73f1308f0a68.png" alt="image.png" /></p>
<p>「Connect with」でPrismaを選択すると、<code>.env</code>と<code>schema.prisma</code>が表示されます。<br />
<code>.env</code>はプロジェクトルートに、<code>schema.prisma</code>はprismaディレクトリに既に作成されているので、表示された内容でファイルを書き換えてください。</p>
<h2 id="Prismaでデータベーステーブルを作成する"><a href="#Prisma%E3%81%A7%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Prismaでデータベーステーブルを作成する</a></h2>
<p>Prisma Migrateでデータベースのテーブルを作っていきます。<br />
Prismaの公式チュートリアルも参考にしてください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-planetscale">https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-planetscale</a></p>
<p>先ほど書き換えた<code>schema.prisma</code>に、データベースのデータモデルを追加します。</p>
<p>prisma/schema.prisma</p>
<pre><code>generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
}
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?
}
</code></pre>
<p>Prismaのスキーマは独自記法なので、意味不明だと思います。以下の公式リファレンスを適宜参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference">https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference</a></p>
<p>簡単な説明だけすると、modelの後に書いたもの(Post、Profile、User)がそれぞれテーブルになります。<br />
波括弧の中で定義しているのは雑に言えばカラムです。<br />
@〇〇(<code>@id</code>など)となっているものはカラムに対する設定です。後ろに括弧を付けると、引数のようにオプションの値を受け取ることができます。<br />
@@〇〇(<code>@@index</code>など)となっているものはテーブルに対する設定です。<br />
IntやStringとなっている部分はカラムの型です。デフォルトはNOT NULLです。<code>?</code>を付けるとNULLABLEになります。<br />
リレーションを張る場合は、モデル自体を示すもの(例えばauthor)と、それに紐づくidを示すもの(例えばauthorId)が必要です。今回はauthorがUserモデルを指し、設定で<code>@relation(fields: [authorId], references: [id])</code>とすることで、authorIdとUser.idが紐づいていることを表しています。詳しくは以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations">https://www.prisma.io/docs/concepts/components/prisma-schema/relations</a></p>
<p><code>posts Post[]</code>のような感じで、一対多を表すこともできます。<br />
多対多については以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations">https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations</a></p>
<p>ここまでできたら早速PlanetScaleにテーブル定義を反映させてみましょう。</p>
<pre><code class="sh">yarn prisma db push
</code></pre>
<p>PlanetScaleで<code>main</code>ブランチを選択し、Schemaタブを開くと、先ほど定義したスキーマがSQLに変換されて表示されているはずです。</p>
<h2 id="NestJSのサービスを構成"><a href="#NestJS%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%92%E6%A7%8B%E6%88%90">NestJSのサービスを構成</a></h2>
<p>PrismaとNestJSを繋げるために、<code>src</code>ディレクトリ内に<code>prisma.service.ts</code>を作ってください。</p>
<p>src/prisma.service.ts</p>
<pre><code>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();
});
}
}
</code></pre>
<p><code>@Injectable()</code>という見慣れない書き方が出てきたと思いますが、これは<strong>デコレータ</strong>と言います。Pythonとかだと馴染みある機能だと思いますが、JavaScriptではまだ実験的な機能のようです。詳しく知りたい方は以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/miruoon_892/articles/365675fa5343ed">https://zenn.dev/miruoon_892/articles/365675fa5343ed</a></p>
<p>NestJSではデコレータを利用した書き方がたくさん出てきます。<br />
デコレータは、後ろに続くメソッドやプロパティをラップできます。そのおかげで、シンプルなコードで強力な機能が使えるようになるわけです。</p>
<p>さて、次にモデルごとに便利な操作関数を作りましょう。<br />
<code>src/user.service.ts</code>と<code>src/post.service.ts</code>を作ります。</p>
<p>src/user.service.ts</p>
<pre><code>import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from '@prisma/client';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
): Promise<User | null> {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
include: {
posts: true,
},
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
include: {
posts: true,
},
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({
data,
include: {
posts: true,
},
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
return this.prisma.user.update({
data,
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
return this.prisma.user.delete({
where,
});
}
}
</code></pre>
<p>src/post.service.ts</p>
<pre><code>import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Post, Prisma } from '@prisma/client';
@Injectable()
export class PostService {
constructor(private prisma: PrismaService) {}
async post(
postWhereUniqueInput: Prisma.PostWhereUniqueInput,
): Promise<Post | null> {
return this.prisma.post.findUnique({
where: postWhereUniqueInput,
include: {
author: true,
},
});
}
async posts(params: {
skip?: number;
take?: number;
cursor?: Prisma.PostWhereUniqueInput;
where?: Prisma.PostWhereInput;
orderBy?: Prisma.PostOrderByWithRelationInput;
}): Promise<Post[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.post.findMany({
skip,
take,
cursor,
where,
orderBy,
include: {
author: true,
},
});
}
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
return this.prisma.post.create({
data,
include: {
author: true,
},
});
}
async updatePost(params: {
where: Prisma.PostWhereUniqueInput;
data: Prisma.PostUpdateInput;
}): Promise<Post> {
const { data, where } = params;
return this.prisma.post.update({
data,
where,
include: {
author: true,
},
});
}
async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
return this.prisma.post.delete({
where,
});
}
}
</code></pre>
<p><code>include: { posts: true }</code>や<code>include: { author: true }</code>というのは、リレーションクエリの設定です。デフォルトではリレーションするデータを取得することはできないので、明示的に指定する必要があります。<br />
後述するGraphQLでもこのサービスを再利用するので重要です。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries">https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries</a></p>
<p>具体的には、リレーションクエリを指定しないと以下のようなレスポンスになります。(後述する動作確認の段階まで進むと実行できるようになります。)</p>
<pre><code class="sh">curl http://localhost:3000/post/1
{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1}
</code></pre>
<p>リレーションクエリを指定すると以下のようになります。</p>
<pre><code class="sh">curl http://localhost:3000/post/1
{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1,"author":{"id":1,"email":"test.jp","name":"namosuke"<span>}</span><span>}</span>
</code></pre>
<h2 id="NestJSのコントローラを構成"><a href="#NestJS%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%82%92%E6%A7%8B%E6%88%90">NestJSのコントローラを構成</a></h2>
<p>最後に、コントローラを書いて、APIのエンドポイントと便利関数を繋げます。</p>
<p>src/app.controller.ts</p>
<pre><code>import {
Controller,
Get,
Param,
Post,
Body,
Put,
Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { PostService } from './post.service';
import { User as UserModel, Post as PostModel } from '@prisma/client';
@Controller()
export class AppController {
constructor(
private readonly userService: UserService,
private readonly postService: PostService,
) {}
@Get('post/:id')
async getPostById(@Param('id') id: string): Promise<PostModel> {
return this.postService.post({ id: Number(id) });
}
@Get('feed')
async getPublishedPosts(): Promise<PostModel[]> {
return this.postService.posts({
where: { published: true },
});
}
@Get('filtered-posts/:searchString')
async getFilteredPosts(
@Param('searchString') searchString: string,
): Promise<PostModel[]> {
return this.postService.posts({
where: {
OR: [
{
title: { contains: searchString },
},
{
content: { contains: searchString },
},
],
},
});
}
@Post('post')
async createDraft(
@Body() postData: { title: string; content?: string; authorEmail: string },
): Promise<PostModel> {
const { title, content, authorEmail } = postData;
return this.postService.createPost({
title,
content,
author: {
connect: { email: authorEmail },
},
});
}
@Post('user')
async signupUser(
@Body() userData: { name?: string; email: string },
): Promise<UserModel> {
return this.userService.createUser(userData);
}
@Put('publish/:id')
async publishPost(@Param('id') id: string): Promise<PostModel> {
return this.postService.updatePost({
where: { id: Number(id) },
data: { published: true },
});
}
@Delete('post/:id')
async deletePost(@Param('id') id: string): Promise<PostModel> {
return this.postService.deletePost({ id: Number(id) });
}
}
</code></pre>
<p>GETリクエストの場合は<code>@Get('post/:id')</code>みたいなデコレータでエンドポイントから受け取るパラメータを指定して、<code>@Param('id')</code>に流すみたいなそんな感じですね。<br />
POSTリクエストの場合は<code>@Post</code>から<code>@Body() postData: { title: string; content?: string; authorEmail: string }</code>みたいにして受け取ります。<br />
ちなみに、<code>@Param</code>も<code>@Body</code>も中身はString型になります。</p>
<p>公式チュートリアルはここで終わりですが、このままだとエラーが出るので以下のように<code>src/app.module.ts</code>を書き換えて修正します。<br />
<code>providers</code>にサービスを追加しています。</p>
<p>src/app.module.ts</p>
<pre><code>import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { UserService } from './user.service';
import { PostService } from './post.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, PrismaService, UserService, PostService],
})
export class AppModule {}
</code></pre>
<p>ここまで書けばREST APIの実装は終わりです。</p>
<h2 id="REST APIの動作確認"><a href="#REST+API%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">REST APIの動作確認</a></h2>
<p>動作確認方法は以下に詳しく書かれています。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59#%E8%B5%B7%E5%8B%95%E3%81%A8%E7%A2%BA%E8%AA%8D">https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59#%E8%B5%B7%E5%8B%95%E3%81%A8%E7%A2%BA%E8%AA%8D</a></p>
<p><code>yarn start</code>したら http://localhost:3000 にサーバが立つので、curlとか使って動作するか試してみてください。</p>
<h3 id="実行例"><a href="#%E5%AE%9F%E8%A1%8C%E4%BE%8B">実行例</a></h3>
<pre><code class="sh">curl -XPOST -d 'name=test3&[email protected]' http://localhost:3000/user
{"id":4,"email":"[email protected]","name":"test3","posts":[]}
curl -XPOST -d 'title=niceTitle&content=niceContent&[email protected]' http://localhost:3000/post
{"id":3,"title":"niceTitle","content":"niceContent","published":false,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>
curl -XPUT http://localhost:3000/publish/3
{"id":3,"title":"niceTitle","content":"niceContent","published":true,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>
curl http://localhost:3000/feed
[{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1,"author":{"id":1,"email":"test.jp","name":"namosuke"<span>}</span><span>}</span>,{"id":2,"title":"はろー","content":"コンテンツ","published":true,"authorId":3,"author":{"id":3,"email":"[email protected]","name":"test"<span>}</span><span>}</span>,{"id":3,"title":"niceTitle","content":"niceContent","published":true,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>]
</code></pre>
<h2 id="Prisma Studioの利用"><a href="#Prisma+Studio%E3%81%AE%E5%88%A9%E7%94%A8">Prisma Studioの利用</a></h2>
<p>データベースの中身をいじりたいときはPrisma Studioを使うと楽ちんです。機能がシンプルになったphpMyAdminみたいなイメージです。</p>
<pre><code class="sh">yarn prisma studio
</code></pre>
<p>と入力すると、 http://localhost:5555 でPrisma Studioが立ち上がります。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/73364eb6-d89c-236b-9c51-5a163923cebd.png" alt="image.png" /></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/21f5f09e-9b53-4da8-9fae-9cea6a023b46.png" alt="image.png" /></p>
<p>データの簡単な追加、修正はここでやれば良さそうです。</p>
<h2 id="シードの利用"><a href="#%E3%82%B7%E3%83%BC%E3%83%89%E3%81%AE%E5%88%A9%E7%94%A8">シードの利用</a></h2>
<p>開発していると、初期データとして同じレコードを一度に投入したくなることがあります。<br />
特にPlanetScaleでは<code>main</code>ブランチを<code>production</code>ブランチに統合する際にすべてのレコードが失われるので、必要性が高いでしょう。<br />
そんなときは、Prismaを利用してシードスクリプトを作成すると便利です。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/guides/database/seed-database">https://www.prisma.io/docs/guides/database/seed-database</a></p>
<h2 id="APIドキュメントを自動生成"><a href="#API%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90">APIドキュメントを自動生成</a></h2>
<p>NestJSにはOpenAPI形式のドキュメントを扱うフレームワークSwaggerを利用して、APIドキュメントを自動生成してくれる機能があります。<br />
まずは必要なパッケージをインストールしましょう。</p>
<pre><code class="sh">yarn add @nestjs/swagger
</code></pre>
<p>次に、<code>src/main.ts</code>でSwaggerを初期化します。</p>
<p>src/main.ts</p>
<pre><code>import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('ユーザ投稿API')
.setDescription('ユーザが投稿できるAPIです')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
</code></pre>
<p>あとは<code>yarn start</code>するだけで、 http://localhost:3000/api に自動的にSwaggerのドキュメントページが立ち上がります。<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/5bed8412-ec44-03f2-0d3c-36a77aec3fc3.png" alt="image.png" /></p>
<p>ドキュメント内で実際にAPIを実行してみることもできます。<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/aa857f9d-0d13-df1c-19ba-ca5aec109d84.png" alt="image.png" /></p>
<p>ドメインルートにドキュメントを設置したい場合は</p>
<pre><code class="ts">SwaggerModule.setup('api', app, document);
</code></pre>
<p>となっている部分を</p>
<pre><code class="ts">SwaggerModule.setup('', app, document);
</code></pre>
<p>に変えることで、 http://localhost:3000 で表示できるようになります。</p>
<p>ちなみに、OpenAPIのJSONでの定義ファイルは http://localhost:3000/api-json からダウンロードできます。<br />
同様にYAMLでの定義ファイルは http://localhost:3000/api-yaml からダウンロードできます。<br />
(ドキュメントをドメインルートに設置している場合はそれぞれ http://localhost:3000/-json 、 http://localhost:3000/-yaml からダウンロードできます。)</p>
<h1 id="GraphQLの実装"><a href="#GraphQL%E3%81%AE%E5%AE%9F%E8%A3%85">GraphQLの実装</a></h1>
<p>GraphQLはREST APIの進化版のようなものです。一つの処理のために何度もAPIを呼んだり、実際にAPIを呼ぶまでレスポンスの形式がわからなかったりといった苦痛を解消してくれます。詳しくは以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/">https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/</a></p>
<p>ここからは以下を参考にしていきます。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://docs.nestjs.com/graphql/quick-start">https://docs.nestjs.com/graphql/quick-start</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/rince/articles/50a66241d04f0b">https://zenn.dev/rince/articles/50a66241d04f0b</a></p>
<p>必要なパッケージをインストールしていきます。</p>
<pre><code class="sh">yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
</code></pre>
<p>GraphQLの開発では、コードからスキーマを生成する<strong>コードファースト</strong>と、スキーマからコードを生成する<strong>スキーマファースト</strong>という2つのアプローチがあります。<br />
どちらにせよ処理に必要なコードを書かないといけないので、コードファーストのほうが良いと思います。コードファーストで進めます。</p>
<h2 id="NestJSのモジュールを構成"><a href="#NestJS%E3%81%AE%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E6%A7%8B%E6%88%90">NestJSのモジュールを構成</a></h2>
<p>AppModuleに色々追加していきます。</p>
<p>src/app.module.ts</p>
<pre><code>import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import { PostService } from './post.service';
import { PostResolver } from './post.resolver';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
],
controllers: [AppController],
providers: [
AppService,
PrismaService,
UserService,
UserResolver,
PostService,
PostResolver,
],
})
export class AppModule {}
</code></pre>
<h2 id="NestJSのモデルを構成"><a href="#NestJS%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%82%92%E6%A7%8B%E6%88%90">NestJSのモデルを構成</a></h2>
<p>続いて、GraphQLのスキーマとして必要な型を設定していきます。<br />
<code>src/user.model.ts</code>と<code>src/post.model.ts</code>を作ります。</p>
<p>src/user.model.ts</p>
<pre><code>import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Post } from 'src/post.model';
@ObjectType()
export class User {
@Field((type) => ID)
id: number;
@Field()
email: string;
@Field({ nullable: true })
name: string | null;
@Field((type) => [Post], { nullable: true })
posts: Post[] | null;
}
</code></pre>
<p>src/post.model.ts</p>
<pre><code>import { Field, ID, ObjectType } from '@nestjs/graphql';
import { User } from 'src/user.model';
@ObjectType()
export class Post {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field({ nullable: true })
content?: string;
@Field()
published: boolean;
@Field((type) => User, { nullable: true })
author?: User;
}
</code></pre>
<p>型を使うために相互に参照し合っているのが面白いですね。<br />
<code>@Field</code>には、曖昧さを無くすためにGraphQLの型を指定できます。例えばTypeScriptの型<code>: number</code>では<code>Int</code>なのか<code>Float</code>なのか<code>ID</code>なのかわからないので、明示的に指定してあげましょう。</p>
<p>記述する際には<code>prisma/schema.prisma</code>を見ながら書くと楽です。<code>prisma/schema.prisma</code>からモデルを自動生成してくれる非公式パッケージ(<a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/prisma-nestjs-graphql">prisma-nestjs-graphql</a>)もあるようですが、動作に不安があるので手書きのほうが安心だと思います。</p>
<h2 id="NestJSのリゾルバを構成"><a href="#NestJS%E3%81%AE%E3%83%AA%E3%82%BE%E3%83%AB%E3%83%90%E3%82%92%E6%A7%8B%E6%88%90">NestJSのリゾルバを構成</a></h2>
<p>いよいよGraphQL版のコントローラみたいなやつ、リゾルバを書いていきます。<br />
ここに書かれたメソッドが、そのままGraphQLから呼び出せるようになります。<br />
<code>src/user.resolver.ts</code>と<code>src/post.resolver.ts</code>を作成します。</p>
<p>src/user.resolver.ts</p>
<pre><code>import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UserService } from 'src/user.service';
import { User } from './user.model';
@Resolver(() => User)
export class UserResolver {
constructor(private userService: UserService) {}
@Query(() => [User])
async users() {
return this.userService.users({});
}
@Query(() => User)
async user(@Args('id') id: number) {
return this.userService.user({ id });
}
@Mutation(() => User)
async createPost(@Args('email') email: string, @Args('name') name: string) {
return this.userService.createUser({ email, name });
}
}
</code></pre>
<p>src/post.resolver.ts</p>
<pre><code>import {
Args,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { PostService } from 'src/post.service';
import { UserService } from './user.service';
import { Post } from './post.model';
@Resolver(() => Post)
export class PostResolver {
constructor(
private postService: PostService,
private userService: UserService,
) {}
@Query(() => [Post])
async posts() {
return this.postService.posts({});
}
@Query(() => Post)
async post(@Args('id') id: number) {
return this.postService.post({ id });
}
@Mutation(() => Post)
async createPost(
@Args('title') title: string,
@Args('content') content: string,
) {
return this.postService.createPost({ title, content });
}
@ResolveField()
async author(@Parent() post: Post) {
return this.userService.user({ id: post.author.id });
}
}
</code></pre>
<p>REST APIを作成するときに作ったサービスをそのまま使っています。<br />
サービスに作った便利関数をREST APIでもGraphQLでも使えるわけですね。</p>
<p>ちなみに、<code>@ResolveField()</code>という部分では、入れ子にしてデータを深掘って取得できるフィールドを指定しています。<br />
これが無いと、REST APIで取得できる以上のデータが取得できず、せっかくのGraphQLの強みが活かせません。<br />
例えば今回は<code>post</code>のリゾルバに<code>author</code>を指定しているので、特定の投稿から著者を取得し、さらに著者の持つ全ての投稿を同時に取得できるようになります。</p>
<p><code>@ResolveField()</code>を設定しなかった場合:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/428011f8-90ab-d9c5-900a-6e9ab4c5e905.png" alt="image.png" /></p>
<p><code>@ResolveField()</code>を設定した場合:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/ddca20a3-3043-8354-336a-5095554a8654.png" alt="image.png" /></p>
<h2 id="GraphQLの動作確認"><a href="#GraphQL%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">GraphQLの動作確認</a></h2>
<p>これでおしまい!<br />
<code>yarn start:dev</code>したあとに http://localhost:3000/graphql を開いてplaygroundを確認してみましょう。</p>
<p>スキーマを確認したり…<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/cf3f1c57-b90e-940b-772b-e2cfda55cf65.png" alt="image.png" /></p>
<p>自動生成されたdocsを確認したり…<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/9dc6fee9-adf5-cc85-aefd-daca010e6188.png" alt="image.png" /></p>
<p>クエリを投げてみたり…<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/2cca6608-5ec3-af51-4bad-6f93db7683ce.png" alt="image.png" /></p>
<p>大丈夫そうですね!ばっちりです!</p>
ウラル