TL;DR
- NestJS では、Code first で TypeScript のクラスにデコレータを付けると、開発サーバの起動時に GraphQL Schema が生成される。
- Mutation や Query の引数を
@InputTypeや@ArgsTypeのクラスにまとめ、ValidationPipeを使うと、class-validator によるバリデーションができる。
検証
セットアップ
公式ドキュメントに沿って、必要なパッケージをインストールする。デフォルトでは Apollo を使う。
$ npm i @nestjs/graphql @nestjs/apollo @apollo/server graphqlルートモジュールで GraphQLModule.forRoot をインポートし、autoSchemaFile に GraphQL Schema を生成するパスを指定する。
// app.module.ts
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver, // 使用するドライバ
playground: true, // プレイグラウンドを使うか
autoSchemaFile: join(process.cwd(), "src/schema.gql"), // GraphQL Schema を生成するパス
}),
],
})
export class AppModule {}TypeScript の型定義から GraphQL Schema を生成する
オブジェクト型は、@ObjectType と @Field デコレータで定義する。
// src/task/models/task.model.ts
import { Field, Int, ObjectType } from "@nestjs/graphql"
@ObjectType()
export class Task {
@Field(() => Int)
id: number
@Field()
name: string
@Field()
dueDate: string
@Field()
status: "NOT_STARTED" | "IN_PROGRESS" | "COMPLETED"
@Field({ nullable: true })
description: string
}TypeScript の number は Float に変換されるので、Int にしたいときは @Field で明示する。オプショナルにしたいときは、@Field の引数に { nullable: true } を渡す。@Field には、ほかに description(フィールドの説明)、name(上書き後のフィールド名)、defaultValue(デフォルト値)を指定できる(ドキュメント)。
Resolver を定義する
Resolver は nest g resolver コマンドでも生成できる(ドキュメント)。ロジックは Service に書く。手で定義するときは、次のようにする。
- クラスに
@Resolverデコレータを付ける。 - 実現したい処理をメソッドとして定義し、Query なら
@Query、Mutation なら@Mutationデコレータを付ける。Queryは@nestjs/commonではなく@nestjs/graphqlからimportする。これらのデコレータには、戻り値の GraphQL の型をアロー関数の形で渡す。
@Resolver()
export class TaskResolver {
constructor(private readonly taskService: TaskService) {}
@Query(() => [Task], { nullable: 'items' })
getTasks(): Task[] {
...
}
@Mutation(() => Task)
createTask(
@Args('name') name: string,
@Args('dueDate') dueDate: string,
@Args('description', { nullable: true }) description: string,
): Task {
...
}
}@Query や @Mutation の nullable には、要素が空のときに空配列を返す 'items'([String]!)や、null を返す 'itemsAndList'([String])を指定できる。@Field と同じく、name で名前を上書きすることもできる。
リクエストで受け取る値はメソッドの引数になり、@Args にはリクエストで使うフィールド名を指定する。
mutation createTask($name: String!, $dueDate: String!, $description: String!) {
createTask(name: $name, dueDate: $dueDate, description: $description) {
id
name
dueDate
status
description
}
}Pipe でバリデーションする
Mutation に渡す値は、@InputType を使って別のクラスとして表せる。これは GraphQL の Input types に当たる。
// createTask.input.ts
import { Field, InputType } from "@nestjs/graphql"
import { IsDateString, IsNotEmpty } from "class-validator"
@InputType()
export class CreateTaskInput {
@Field()
@IsNotEmpty()
name: string
@Field()
@IsDateString()
dueDate: string
@Field({ nullable: true })
description?: string
}このクラスには、DTO のようにバリデーションの責務を持たせられる。ただし、バリデーションを実行するには NestJS の Pipe を使う必要がある。Pipe は、Controller や Resolver がリクエストを受け取る前に、リクエストに処理を加える機能である。
まず、組み込みの ValidationPipe をグローバルに設定する。
// main.ts
import { ValidationPipe } from "@nestjs/common"
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())
// ...
}次に、Resolver と Service の引数で @InputType のクラスのインスタンスを受け取るようにする。
@Resolver()
export class TaskResolver {
// ...
@Mutation(() => Task)
createTask(@Args("createTaskInput") createTaskInput: CreateTaskInput): Task {
return this.taskService.createTask(createTaskInput)
}
}@Injectable()
export class TaskService {
// ...
createTask(createTaskInput: CreateTaskInput): Task {
const { name, dueDate, description } = createTaskInput
return await this.prismaService.task.create({
data: { name, dueDate, description },
})
}
}Query に渡す値も、@ArgsType を使って別のクラスとして表せる。@Mutation のときと違い、@Args に名前の文字列を渡さない。
@ArgsType()
export class GetUserArgs {
@Field()
@IsEmail()
email: string
}@Query(() => UserModel, { nullable: true })
async getUser(@Args() getUserArgs: GetUserArgs): Promise<User> {
return await this.userService.getUser(getUserArgs.email);
}Playground
GraphQLModule.forRoot に { playground: true } を渡して開発サーバを起動すると、http://localhost:3000/graphql で Playground が使える(ドキュメント)。
結果
npm run start:dev を実行すると、開発サーバの起動と同時に、次の GraphQL Schema が生成された。
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Task {
id: Float!
name: String!
dueDate: String!
status: String!
description: String
}
type Query {
getTasks: [Task]!
}ValidationPipe を設定した後に不正なリクエストを送ると、次のエラーが返ってきた。
{
"errors": [
{
"message": "Bad Request Exception",
"locations": [{ "line": 2, "column": 3 }],
"path": ["createTask"],
"extensions": {
"code": "BAD_REQUEST",
"stacktrace": ["..."],
"originalError": {
"message": ["name should not be empty", "dueDate must be a valid ISO 8601 date string"],
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}