NestJS の GraphQL 機能を試してみる
このスクラップについて
このスクラップでは NestJS の公式ドキュメントに従って GraphQL 機能を試してみる
インストール
nest new -p npm hello-nestjs-graphql
cd hello-nestjs-graphql
npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express
GraphQLModule のインポート
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
開発サーバー起動
npm run start:dev
この時点では GraphQL の設定が完了していないのでエラーメッセージが表示される
/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/apollo-server-core/src/ApolloServer.ts:677
throw Error(
^
Error: Apollo Server requires either an existing schema, modules or typeDefs
at ApolloServer.constructSchema (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/apollo-server-core/src/ApolloServer.ts:677:13)
at new ApolloServerBase (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/apollo-server-core/src/ApolloServer.ts:330:18)
at new ApolloServer (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/apollo-server-express/src/ApolloServer.ts:55:1)
at ApolloDriver.registerExpress (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/apollo/dist/drivers/apollo-base.driver.js:77:30)
at ApolloDriver.registerServer (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/apollo/dist/drivers/apollo.driver.js:38:24)
at ApolloDriver.start (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/apollo/dist/drivers/apollo.driver.js:23:20)
at GraphQLModule.onModuleInit (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/graphql/dist/graphql.module.js:105:9)
at callModuleInitHook (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/core/hooks/on-module-init.hook.js:51:9)
at NestApplication.callInitHook (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/core/nest-application-context.js:224:13)
at NestApplication.init (/Users/susukida/workspace/js/hello-nestjs-graphql/node_modules/@nestjs/core/nest-application.js:98:9)
Quick start では手順は詳しく説明されていない
詳しくは下記の Example を見てね、ということで丸投げされている
参考になりそうな Web ページ
Module の生成
nest g module posts
nest g module authors
生成されるファイルがこちら
import { Module } from '@nestjs/common';
@Module({})
export class PostsModule {}
import { Module } from '@nestjs/common';
@Module({})
export class AuthorsModule {}
app.module.ts も自動的に変更してくれる
import { ApolloDriver, ApolloDriverConfig } 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 { AuthorsModule } from './authors/authors.module'; // この行が自動的に追加される
import { PostsModule } from './posts/posts.module'; // この行が自動的に追加される
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
AuthorsModule, // この行が自動的に追加される
PostsModule, // この行が自動的に追加される
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Object type の作成
mkdir -p src/posts/models
mkdir -p src/authors/models
touch src/posts/models/post.model.ts
touch src/authors/models/author.model.ts
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field((type) => Int)
id: number;
@Field()
title: string;
@Field((type) => Int, { nullable: true })
votes?: number;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from '../../posts/models/post.model';
@ObjectType()
export class Author {
@Field((type) => Int)
id: number;
@Field({ nullable: true })
firstName?: string;
@Field({ nullable: true })
lastName?: string;
@Field((type) => [Post])
posts: Post[];
}
なかなかうまくいかない
公式ドキュメントではなくて GitHub の Example を参考にした方が良さそう
仕切り直し
一旦リセットして GitHub の Example を参考にして進めていく。
まずはワークスペースの準備。
rm -rf hello-nestjs-graphql
nest new -p npm hello-graphql
cd hello-graphql
npm install --save @nestjs/graphql @nestjs/apollo graphql apollo-server-express
App Module のコーディング
import { ApolloDriver, ApolloDriverConfig } 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';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
CRUD 生成
下記のページで知った CRUD 生成を使ってみる。
# nest g res recipes でも OK です
nest generate resource recipes
? What transport layer do you use?
REST API
❯ GraphQL (code first)
GraphQL (schema first)
Microservice (non-HTTP)
WebSockets
? Would you like to generate CRUD entry points? (Y/n) Y
CREATE src/recipes/recipes.module.ts (238 bytes)
CREATE src/recipes/recipes.resolver.spec.ts (545 bytes)
CREATE src/recipes/recipes.resolver.ts (1181 bytes)
CREATE src/recipes/recipes.service.spec.ts (467 bytes)
CREATE src/recipes/recipes.service.ts (653 bytes)
CREATE src/recipes/dto/create-recipe.input.ts (198 bytes)
CREATE src/recipes/dto/update-recipe.input.ts (251 bytes)
CREATE src/recipes/entities/recipe.entity.ts (189 bytes)
UPDATE src/app.module.ts (685 bytes)
src/app.module.ts で自動的に RecipesModule がインポートされている。
生成されたソースコードを眺めてみる
import { Module } from '@nestjs/common';
import { RecipesService } from './recipes.service';
import { RecipesResolver } from './recipes.resolver';
@Module({
providers: [RecipesResolver, RecipesService]
})
export class RecipesModule {}
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { RecipesService } from './recipes.service';
import { Recipe } from './entities/recipe.entity';
import { CreateRecipeInput } from './dto/create-recipe.input';
import { UpdateRecipeInput } from './dto/update-recipe.input';
@Resolver(() => Recipe)
export class RecipesResolver {
constructor(private readonly recipesService: RecipesService) {}
@Mutation(() => Recipe)
createRecipe(
@Args('createRecipeInput') createRecipeInput: CreateRecipeInput,
) {
return this.recipesService.create(createRecipeInput);
}
@Query(() => [Recipe], { name: 'recipes' })
findAll() {
return this.recipesService.findAll();
}
@Query(() => Recipe, { name: 'recipe' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.recipesService.findOne(id);
}
@Mutation(() => Recipe)
updateRecipe(
@Args('updateRecipeInput') updateRecipeInput: UpdateRecipeInput,
) {
return this.recipesService.update(updateRecipeInput.id, updateRecipeInput);
}
@Mutation(() => Recipe)
removeRecipe(@Args('id', { type: () => Int }) id: number) {
return this.recipesService.remove(id);
}
}
import { Injectable } from '@nestjs/common';
import { CreateRecipeInput } from './dto/create-recipe.input';
import { UpdateRecipeInput } from './dto/update-recipe.input';
@Injectable()
export class RecipesService {
create(createRecipeInput: CreateRecipeInput) {
return 'This action adds a new recipe';
}
findAll() {
return `This action returns all recipes`;
}
findOne(id: number) {
return `This action returns a #${id} recipe`;
}
update(id: number, updateRecipeInput: UpdateRecipeInput) {
return `This action updates a #${id} recipe`;
}
remove(id: number) {
return `This action removes a #${id} recipe`;
}
}
import { InputType, Int, Field } from '@nestjs/graphql';
@InputType()
export class CreateRecipeInput {
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
}
import { CreateRecipeInput } from './create-recipe.input';
import { InputType, Field, Int, PartialType } from '@nestjs/graphql';
@InputType()
export class UpdateRecipeInput extends PartialType(CreateRecipeInput) {
@Field(() => Int)
id: number;
}
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class Recipe {
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
}
生成されるスキーマ
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Recipe {
"""Example field (placeholder)"""
exampleField: Int!
}
type Query {
recipes: [Recipe!]!
recipe(id: Int!): Recipe!
}
type Mutation {
createRecipe(createRecipeInput: CreateRecipeInput!): Recipe!
updateRecipe(updateRecipeInput: UpdateRecipeInput!): Recipe!
removeRecipe(id: Int!): Recipe!
}
input CreateRecipeInput {
"""Example field (placeholder)"""
exampleField: Int!
}
input UpdateRecipeInput {
"""Example field (placeholder)"""
exampleField: Int
id: Int!
}
Query 実行
http://localhost:3000/graphql にアクセスしてクエリを実行してみる。
サービスを未実装なのでエラーになる。
query {
recipes {
exampleField
}
}
{
"errors": [
{
"message": "Expected Iterable, but did not find one for field \"Query.recipes\".",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"recipes"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"message": "Expected Iterable, but did not find one for field \"Query.recipes\".",
"stacktrace": [
"GraphQLError: Expected Iterable, but did not find one for field \"Query.recipes\".",
" at completeListValue (/Users/susukida/workspace/js/hello-graphql/node_modules/graphql/execution/execute.js:668:11)",
" at completeValue (/Users/susukida/workspace/js/hello-graphql/node_modules/graphql/execution/execute.js:607:12)",
" at completeValue (/Users/susukida/workspace/js/hello-graphql/node_modules/graphql/execution/execute.js:584:23)",
" at /Users/susukida/workspace/js/hello-graphql/node_modules/graphql/execution/execute.js:486:9",
" at processTicksAndRejections (node:internal/process/task_queues:96:5)",
" at async Promise.all (index 0)",
" at execute (/Users/susukida/workspace/js/hello-graphql/node_modules/apollo-server-core/src/requestPipeline.ts:501:14)",
" at processGraphQLRequest (/Users/susukida/workspace/js/hello-graphql/node_modules/apollo-server-core/src/requestPipeline.ts:407:22)",
" at processHTTPRequest (/Users/susukida/workspace/js/hello-graphql/node_modules/apollo-server-core/src/runHttpQuery.ts:436:24)"
]
}
}
}
],
"data": null
}
サービス実装
import { Injectable } from '@nestjs/common';
import { CreateRecipeInput } from './dto/create-recipe.input';
import { UpdateRecipeInput } from './dto/update-recipe.input';
import { Recipe } from './entities/recipe.entity';
@Injectable()
export class RecipesService {
create(createRecipeInput: CreateRecipeInput): Recipe {
const recipe = new Recipe();
recipe.exampleField = createRecipeInput.exampleField;
return recipe;
}
findAll(): Recipe[] {
const recipe1 = new Recipe();
recipe1.exampleField = 1;
const recipe2 = new Recipe();
recipe2.exampleField = 2;
const recipe3 = new Recipe();
recipe3.exampleField = 3;
return [recipe1, recipe2, recipe3];
}
findOne(id: number): Recipe {
const recipe = new Recipe();
recipe.exampleField = 1;
return recipe;
}
update(id: number, updateRecipeInput: UpdateRecipeInput): Recipe {
const recipe = new Recipe();
recipe.exampleField = updateRecipeInput.exampleField;
return recipe;
}
remove(id: number): Recipe {
const recipe = new Recipe();
recipe.exampleField = 1;
return recipe;
}
}
再度クエリ実行
query {
recipes {
exampleField
}
}
{
"data": {
"recipes": [
{
"exampleField": 1
},
{
"exampleField": 2
},
{
"exampleField": 3
}
]
}
}
とりあえずできた!
次回
理解を深めるために nest generate resource
を使わずに実装してみよう。
nest generate resource
を使わない実装
まずはワークスペースの作成から。
rm -rf hello-nestjs-graphql
nest new -p npm hello-nestjs-graphql
cd hello-nestjs-graphql
npm install --save @nestjs/graphql @nestjs/apollo graphql apollo-server-express
touch src/app.resolver.ts
npm run start:dev
GraphQL モジュール読み込み
import { ApolloDriver, ApolloDriverConfig } 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';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.graphql'),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
リゾルバーのコーディング
import { Field, Int, ObjectType, Query, Resolver } from '@nestjs/graphql';
@ObjectType()
export class Author {
@Field(() => Int)
id: number;
}
@Resolver(() => Author)
export class AppResolver {
@Query(() => [Author])
authors(): Author[] {
const author1 = new Author();
const author2 = new Author();
author1.id = 1;
author2.id = 2;
return [author1, author2];
}
}
モジュールでリゾルバーを読み込むのを忘れない。
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppResolver } from './app.resolver'; // この行を追加しました。
import { AppService } from './app.service';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.graphql'),
}),
],
controllers: [AppController],
providers: [AppService, AppResolver], // この行を変更しました。
})
export class AppModule {}
クエリ実行
ブラウザで http://localhost:3000/graphql にアクセスする。
左側のクエリ入力部に下記のクエリを入力する。
Mac の場合は Ctrl + Space でコード補完をしてくれるので便利。
query {
authors {
id
}
}
クエリを実行する。
Mac の場合は Command + Enter で実行できるので便利。
{
"data": {
"authors": [
{
"id": 1
},
{
"id": 2
}
]
}
}
無事にクエリ実行結果が表示された。
初回は何を間違えたのだろう?
次回
次は GraphQL CLI プラグインを試してみる。
ドキュメントによると ObjectType の Field アノテーションをかなり省略できるらしい。
フィールドに一つ一つアノテーションを追加するのは大変なのでこれは便利そう。
GraphQL CLI プラグイン
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
{
"name": "@nestjs/graphql",
"options": {
"typeFileNameSuffix": [".resolver.ts"],
"introspectComments": true
}
}
]
}
}
import { Field, ID, ObjectType, Query, Resolver } from '@nestjs/graphql';
@ObjectType()
export class Author {
/**
* ユーザーの ID です。
*/
@Field(() => ID)
id: number;
/**
* ユーザーの氏名の名の部分です。
*/
firstName?: string;
/**
* ユーザーの氏名の姓の部分です。
*/
lastName?: string;
}
@Resolver(() => Author)
export class AppResolver {
@Query(() => [Author])
authors(): Author[] {
const author1 = new Author();
const author2 = new Author();
author1.id = 1;
author2.id = 2;
return [author1, author2];
}
}
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Author {
"""ユーザーの ID です。"""
id: ID!
"""ユーザーの氏名の名の部分です。"""
firstName: String
"""ユーザーの氏名の姓の部分です。"""
lastName: String
}
type Query {
authors: [Author!]!
}
反映するには npm run start:dev
の再起動が必要。
うまく動かない場合は nest-cli.json の typeFileNameSuffix
を見直すと良い。
次回
ResolveField について試してみよう。
スキーマファーストアプローチについても試してみたい。
ResolveField を試してみる
import {
Field,
ID,
Int,
ObjectType,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
@ObjectType()
export class Author {
@Field(() => ID)
id: number;
@Field()
firstName?: string;
@Field()
lastName?: string;
@Field(() => [Post], { nullable: 'items' })
posts: Post[];
}
@ObjectType()
export class Post {
@Field(() => ID)
id: number;
@Field()
title: string;
@Field(() => Int)
votes?: number;
}
@Resolver(() => Author)
export class AppResolver {
@Query(() => [Author])
authors(): Author[] {
const author1 = new Author();
const author2 = new Author();
author1.id = 1;
author2.id = 2;
return [author1, author2];
}
@ResolveField()
posts(@Parent() author: Author): Post[] {
const post = new Post();
post.id = 1;
post.title = `${author.firstName}の記事タイトル`;
post.votes = 1;
return [post];
}
}
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Author {
id: ID!
firstName: String
lastName: String
posts: [Post]!
}
type Post {
id: ID!
title: String!
votes: Int
}
type Query {
authors: [Author!]!
}
http://localhost:3000/graphql にアクセスして下記のクエリを実行する。
query {
authors {
id
firstName
lastName
posts {
id
title
votes
}
}
}
{
"data": {
"authors": [
{
"id": "1",
"firstName": null,
"lastName": null,
"posts": [
{
"id": "1",
"title": "undefinedの記事タイトル",
"votes": 1
}
]
},
{
"id": "2",
"firstName": null,
"lastName": null,
"posts": [
{
"id": "1",
"title": "undefinedの記事タイトル",
"votes": 1
}
]
}
]
}
}
次回
次は Args デコレーターを試してみる。