NestJsでGraphQLしてみる

#  前置き
件名のとおりだが、Prismaとか、フロントエンドとかゴチャゴチャしたものは省いて、`NestJS` + `Apollo` で `GraphQL` 環境を構築する

※ 2025/01/27 加筆修正

:::note info
この記事のコードは https://github.com/rk7fd3s/nestjs-apollo
:::


# 環境
執筆時の環境は下記の通り
```
node 23.6.1
nestjs/cli 11.0.2
```

----

# 準備
まず、Nest CLI をグローバルにインストール。
```
npm install -g @nestjs/cli
```

nestコマンドでプロジェクトを作成。
```
nest new nestjs-apollo
```
:::note warn
※ nestjs-apollo は任意で
:::

作成後は作成されたディレクトリに移動

----

# 不要なファイルの削除

* src/app.controller.spec.ts
* src/app.controller.ts
* src/app.service.ts

上記のファイルは今回不要なので削除する。

## src/app.module.ts を修正

上記削除したファイルを参照しているsrc/app.module.tsを下記のように修正する。

```
import { Module } from '@nestjs/common';

@Module({
  imports: [],
})
export class AppModule {}
```


# パッケージインストール
NestJSからapplo, graphqlを使うためのパッケージをインストールする。

```
npm i @nestjs/graphql @nestjs/apollo
```

:::note alert
今回 `@apollo/server` や `graphql` は要らない
:::

# 自動的にスキーマファイルを作成するためのコード

src/app.module.ts を下記のように修正します

```
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import * as path from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
    })
  ],
})
export class AppModule {}
```

こうすることにより、`npm run start` でサーバを立ち上げた際に、 `src/schema.gql` にスキーマ情報を吐き出すようになります。

:::note alert
この段階ではスキーマ情報を作成する元が無いのでエラーとなります
:::

----
ここから本番
----

# User
今回試しに、ユーザ情報を格納するモジュールを作成してGraphQLでデータのやり取りをする。

## モジュールとリゾルバー、サービスを作成

```
nest g module user --no-spec
nest g resolver user --no-spec
nest g service user --no-spec
```
:::note alert
今回、スペックファイルは作成しない(作りたい人はどうぞ)
:::

上記コマンドを打つと

src/user/user.module.ts、src/user/user.resolver.ts、src/user/user.serice.ts が作成され、src/app.module.ts が修正される。

:::note info
nestコマンドを使用しないで、各ファイルを作成してもOK.
参考 [905cc96](https://github.com/rk7fd3s/nestjs-apollo/commit/905cc962e0f855751c088421a5336f3ef5737bca)
:::

## モデルを作成

### src/user/user.model.ts

IDと名前、作成日時、更新日時を持つモデルとして作成。

```
import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class UserModel {
  @Field(() => String)
  id: string;

  @Field(() => String)
  name: string;

  @Field(() => Date)
  createdAt: Date;

  @Field(() => Date, { nullable: true })
  updatedAt: Date | null;
}
```

### src/user/user.service.ts

上記モデルの情報を取得、更新するためのサービスを書きます

```
import { Injectable } from '@nestjs/common';
import { UserModel } from './user.model';

const users: UserModel[] = [];
let index: number = 1;

@Injectable()
export class UserService {
  constructor() {
    const dummyUser01: UserModel = {
      id: this.getNewUserId(),
      name: 'name 0001',
      createdAt: new Date('2000-01-01T01:01:01+09:00'),
      updatedAt: null,
    };
    users.push(dummyUser01);
    const dummyUser02: UserModel = {
      id: this.getNewUserId(),
      name: 'name 0002',
      createdAt: new Date('2000-01-23T12:34:56+09:00'),
      updatedAt: null,
    };
    users.push(dummyUser02);
  }

  /**
   * 新しいユーザIDの取得
   *
   * @returns 新しいユーザのID
   */
  getNewUserId = () => {
    const i = String(index++);
    return i.padStart(4, '0');
  };

  /**
   * 全ユーザ取得
   * @returns 全ユーザリスト
   */
  getUsers = (): UserModel[] => {
    return users;
  };

  /**
   * ユーザ情報取得
   *
   * @param id ユーザID
   * @returns ユーザ情報
   */
  getUser = (id: string): UserModel => {
    const user = users.find((user) => user.id === id);
    if (!user) {
      throw new Error('not found.');
    }
    return user;
  };

  /**
   * 新規ユーザ情報追加
   *
   * @param name ユーザ名
   * @returns
   */
  createUser = (name: string): UserModel => {
    const newUser = {
      id: this.getNewUserId(),
      name,
      createdAt: new Date(),
      updatedAt: null,
    };
    users.push(newUser);
    return newUser;
  };

  /**
   * ユーザ情報を更新
   * 
   * @param id ユーザID
   * @param name ユーザ名
   * @returns ユーザ情報
   */
  updateUser = (id: string, name: string) => {
    const user = users.find((user) => user.id === id);
    if (!user) {
      throw new Error('not found.');
    }
    user.name = name;
    user.updatedAt = new Date();
    return user;
  }

  /**
   * ユーザ情報を削除
   * 
   * @param id ユーザID
   * @returns ユーザ情報
   */
  deleteUser = (id: string) => {
    const user = users.find((user) => user.id === id);
    if (!user) {
      throw new Error('not found.');
    }

    for (let i = 0; i <= users.length; i++) {
      const user = users.shift()!;
      if (user.id !== id) {
        users.push(user);
      }
    }

    return user;
  }
}
```
今回、DBなど永続的記録をしないので、`const users: UserModel[] = [];` の中のデータを操作しています。
詳細な実装はお任せします。

### src/user/user.resolver.ts

上記サービスを使って、GraphQLのAPIとしてリゾルバー登録していきます。

```
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UserModel } from './user.model';
import { UserService } from './user.service';

@Resolver(() => UserModel)
export class UserResolver {
  constructor(private readonly userService: UserService) {}

  // 全ユーザ取得
  @Query(() => [UserModel])
  async users() {
    return this.userService.getUsers();
  }

  // id指定でユーザ取得
  @Query(() => UserModel)
  async user(@Args('id') id: string) {
    return this.userService.getUser(id);
  }

  // nameを与えてユーザを追加する
  @Mutation(() => UserModel)
  async createUser(@Args('name') name: string): Promise<UserModel> {
    return this.userService.createUser(name);
  }

  // idで指定したユーザのnameを変更する
  @Mutation(() => UserModel)
  async updateUser(
    @Args('id') id: string,
    @Args('name') name: string,
  ): Promise<UserModel> {
    return this.userService.updateUser(id, name);
  }

  // idで指定したユーザを削除する
  @Mutation(() => UserModel)
  async deleteUser(@Args('id') id: string): Promise<UserModel> {
    return this.userService.deleteUser(id);
  }
}

```

# サーバ立ち上げ


```
npm run start:dev
```
`src/schema.gql` が作成されたことが確認できるはずです。

[postman](https://www.postman.com/downloads/)などでリクエストを投げて動作を確認してください。