構成が固まったので、早速 GraphQL バックエンドから構築します。が、その前に、本書のこの先の流れについて説明しておきます。
- NestJS で GraphQL サーバーが動くことを確認する
- Next.js で GraphQL クライアントが動くことを確認する
- DB にデータが入っている想定で、個人ブログの記事一覧画面を作ってみる
- Cloud Run と Cloud SQL へデプロイする
- 記事一覧をブラッシュアップする
- 記事の詳細画面をつくる
GraphQL で一連の開発を体験したあとは、まず優先してデプロイします。先にデプロイしておかないと、完成しきってからデプロイするのは大変です。機能が少ないうちに、CI/CD を組んでローカルの成果物を自動でデプロイできる状況を作っておくと後々楽です。一緒にデプロイもしましょう。
NestJS
GraphQL バックエンドを NestJS で構築します。公式サイトによると、NestJS はプログレッシブ Node.js フレームワークです。オブジェクトのインスタンス管理方法が独特で、Angularの影響を強く受けており、Module と呼ばれるTypeScript の Decoratorsを駆使した仕組みを提供します。やはり文字をみても分かりづらいと思うので早速 GraphQL サーバーをたてていきましょう。NestJS は、内部的に Apollo Server を使っている@nestjs/graphql
というパッケージが使えます。
TypeScript の Decorator という話をしましたが、NestJS は TypeScript を前提としたフレームワークです。ここから作っていくアプリケーションは TypeScript で構築します。GraphQL サーバーを立ち上げるところまでやってみましょう。
インストール
ドキュメントに従いインストールします。
mkdir -p second-step-graphql
cd second-step-graphql
yarn init --yes
# NestJS本体はスターターを利用してインストールします。NestJS CLI を利用
yarn add @nestjs/cli
yarn nest new backend
? Which package manager would you ❤️ to use?
npm
❯ yarn
pnpm
次に generate された backend ディレクトリへ移動し、GraphQL 関連をインストール。本書ではバージョンを9系に固定します。
cd backend
yarn add @nestjs/graphql@9.1.2 graphql@^15 apollo-server-express
インストールは以上で OK です。
コードファーストとスキーマファースト
NestJS で GraphQL サーバーを開発していくにあたり、ふたつ方針の選択肢があります。なお、どちらを採用した場合でも、先の節で示した「Schema、データ、Resolver を用意すれば GraphQL サーバーは動かせる」という原理に変わりありません。この選択は、Schema をどうやって作成するか という違いです。
- コードファースト: GraphQL ライブラリが検知できるよう、TypeScript コード中にヒントとなるスニペットを埋め込む。ライブラリが自動で
schema.gql
を生成する - スキーマファースト:
schema.gql
を絶対王者としてこれを開発者がメンテナンスする。ライブラリが TypeScript の型情報を生成する
本書ではコードファーストアプローチを採用 します。GraphQL の本質を考えると、schema.gql
を中心に据えるほうが理にかなっているかもしれません。しかし実際に開発してみての感想としては、結局主役はデータと Resolver になります。TypeScript の動く実装がなければ GraphQL サーバーも動かせません。大人数が絡むような、大規模な開発ではもしかするとスキーマファーストのほうが合うかもしれませんが、少なくとも本書で扱うレベルの開発作業はコードファーストのほうが楽です。企業様の例としては、メルカリ Shops の BFF レイヤでも NestJS を採用しており、そちらの開発でもコードファーストアプローチを採っているそうです。
また、GraphQL のスキーマ管理は悩ましい課題ですが、NestJS の Code First アプローチはコードに集中することができる良い手法でした。これによってエンジニアはスキーマとコードを同期する必要がなくなり、コードだけを管理すればよくなりました。
GraphQL と TypeScript ではじめる
方針も決まったので、サーバーを立てていきましょう。NestJS はインスタンスの管理にModulesという単位でまとめるアーキテクチャを採用しており、アプリケーションコードも外部ライブラリも、Modules という単位で提供することが基本方針です。こうすることで、
- NestJS の世界で統一的にインスタンス管理できる
- NestJS の世界でインターフェースを統一できる
というメリットがあります。GraphQL ライブラリもこの例に漏れず、Modules として提供されており、app.module.ts
で以下のように書いて初期化します。
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
@Module({
imports: [GraphQLModule.forRoot({})],
})
export class AppModule {}
アプリケーションで利用するすべての Modules は、app.module.ts
で読み込み・初期化します。GraphQLModule
のforRoot()
には GraphQL のオプションを設定できます。コードファーストアプローチを採用する場合、schema.gql
を出力するよう、パスを指定してみましょう。
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import * as path from 'path';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: path.join(process.cwd(), "src/schema.gql"),
sortSchema: true,
}),
],
})
export class AppModule {}
これでObjectType
を用意すれば、schema.gql
が作成されます。
ObjectType の作成
そのObjectType
を定義してみましょう。そのためにまずはModulesを作成します。
yarn nest generate module components/posts
ObjectTypeであるPostModel
を作成します。ブログ投稿を想定して書きます。
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class PostModel {
@Field((type) => String)
id: string;
@Field((type) => String)
title: string;
}
このクラスはレスポンスのシリアライザかつschema.gql
自動生成のための情報です。export interface PostModel {
だとJSへコンパイルした後に型情報が抜けてしまうため、class
としてます。これはNestJSフレームワークの制約です。@Field((type) => String)
の記述がGraphQL Schemaのためのデコレータです。TypeScriptのフィールドに着いているので、どの項目がschema.gql
とどう対応するかわかりやすいですね。
Resolvers(コードファースト)とデータ
SchemaときたのでこんどはResolverとデータです。NestJSではResolverもTypeScriptコードで表現します。
import { Args, Query, Resolver } from '@nestjs/graphql';
import { PostModel } from './interfaces/post.model';
@Resolver((of) => PostModel)
export class PostsResolver {
constructor() {}
@Query(() => [PostModel], { name: 'posts', nullable: true })
async getPosts() {
return [
{
id: '1',
title: 'NestJS is so good.',
},
{
id: '2',
title: 'GraphQL is so good.',
},
];
}
}
-
@Resolver((of) => PostModel)
:PostModel
に相当するスキーマを返すことを宣言しています。つまり、このクラスでPostModel
へ書いたすべてのフィールドのデータを取得できることを意味します -
@Query(() => [PostModel], { name: 'posts', nullable: true })
:posts
というクエリが呼ばれたらこのメソッドを実行する動きになります
PostsModule
の修正
Resolverを定義した後、PostsModule
へ登録します。yarn nest generate module components/posts
したとき一緒に作られたposts.module.ts
を修正します。
import { Module } from '@nestjs/common';
import { PostsResolver } from './post.resolvers';
@Module({
+ providers: [PostsResolver],
})
export class PostsModule {}
このようにNestJSはModuleごとにロジックをカプセル化し、統一的なインターフェースで初期化、インスタンス管理ができるようになっています。
定義したModuleを組み合わせてアプリケーションを構築する - https://docs.nestjs.com/providers
AppModule
の修正、起動
最後に作成したModuleをapp.module.ts
へ登録する...のですが、さきほどyarn nest generate module components/posts
したとき自動で設定されています。
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import * as path from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './components/posts/posts.module';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: path.join(
process.cwd(),
'src/generated/graphql/schema.gql',
),
sortSchema: true,
}),
PostsModule, // これ
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Schema(自動生成)、Resolver、データ(固定データ)がすべて揃いました。これで動くでしょうか。
yarn start:dev
👆このコマンド実行時(コード側に問題がなければ)schema.gql
ファイルが生成されます。http://localhost:3000/
へアクセスすると普通のAPIのほうは動いている様子。
あとはGraphQLサーバーがどうかですが、さきほど Apollo Server を立ち上げるときに使った Apollo Studio からアクセスしてみましょう。GraphQLサーバーのアドレスはhttp://localhost:3000/graphql
としてください。
シンプルな Apollo Server の例で示したのと同じように、Apollo Studio から使えました。フレームワークが異なっても同じ開発体験が得られるというのもGraphQLでサーバーを組む良さといえます。最後に、schema.gql
がどうなっているか確認して終わります。
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type PostModel {
id: String!
title: String!
}
type Query {
posts: [PostModel!]
}
自動生成された痕跡があり、「いじらないでね」と書かれています。TypeScriptのデコレータから生成することで、私たちはschema.gql
のことを考えずにすみます。
この節のまとめ
ソースコード
この節で実行したコードは以下のリポジトリで公開しています。