Open10

GoのREST APIをNestJSへ置き換える作業メモ

shuntakashuntaka

今GoでホスティングしているREST APIをNestとGraphQLに置き換えようと思う。
日々の運動を記録するアプリ。

構成は、App RunnerとSQLite with Litestream。昔記事を書いた。

https://zenn.dev/shuntaka/articles/9d35c4710a4b29

App RunnerとAurora Serverless V1構成でそこまで高くならなそうなので、ゆくゆくはそちらにホスティング先を変えたい。
直でDB更新できるのがメリット。AppRunner + SQLite構成は、App Runnerコンテナに直接exec出来ないので、DBを直で触れないという問題がある。まぁその分低コストなんだけどね、、

一番のモチベは、Auroraと仲良くなりたいだけなんだけどね。

shuntakashuntaka

userモジュールの作成

npx nest generate module users
npx nest generate class users/user
npx nest generate resolver users
npx nest generate service users
mkdir src/users/dto && touch src/users/dto/dto/newUser.input.ts
要素 説明 コマンド 生成されるファイル 更新されるファイル
module npx nest generate module users src/users/users.module.ts src/app.module.ts
class npx nest generate class users/user src/users/user.spec.ts src/users/user.ts
resolver npx nest generate resolver users src/users/users.resolver.spec.ts src/users/users.resolver.ts src/users/users.module.ts
service npx nest generate service users src/users/users.service.spec.ts src/users/users.service.ts src/users/users.module.ts
DTO mkdir src/users/dto && touch src/users/dto/newUser.input.ts src/users/dto/newUser.input.ts

.gqlファイル更新タイミング

@Filedをつけた値が、.gqlファイルのスキーマに出力される。ただこの設定だけでは、.gqlファイルに設定は反映されない。src/users/users.resolver.spec.tsでQueryを定義する必要がある。

src/users/user.ts
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Column, Entity } from 'typeorm';

@Entity()
@ObjectType()
export class User {
  @Field((type) => ID)
  id: number;

  @Field()
  @Column()
  name: string;

  @Field()
  @Column()
  createdAt: number;

  @Field()
  @Column()
  modified: number;
}

定義したclassでテーブルを作成する場合

packages/app/src/app.module.ts
@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: host,
      port: 3306,
      username: username,
      password: password,
      database: dbname,
      entities: [Book, User],  // 👈ここに作成したクラスを定義する
      synchronize: true,
    }),
    BooksModule,
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

つまりテーブル定義はアプリ側でなく、SQLで作成するという方法も可能。そうしたい人も多そう。(descしないと何が作られたか確認できないし...)

shuntakashuntaka

workOutsのようなCamelCaseだとwork-outのようなスネークケースに変換されて出力される

npx nest generate module workOuts
npx nest generate class workOuts/workOut
npx nest generate resolver workOuts
npx nest generate service workOuts
mkdir src/work-outs/dto && touch src/work-outs/dto/newWorkOut.input.ts
shuntakashuntaka

以下のような場合、JOINしてリクエストすれば1回で済むのにアプリの作りとして2回リクエストが必要。解決策を模索中。

  • getUserでユーザー詳細取得
    • userIdでユーザーに紐づく複数の概念の取得
@Resolver((of) => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  // ユーザーに1:複で紐づく概念
  @ResolveField((type) => UserWorkoutConnection)
  userHoge(@Parent() { id }: User, @Args() args?: ConnectionArgs): any {
    // getUserで返却された値のidが@Parent経由で取得可能
    // TODO: idを元に複数の概念の取得

    return {
      edges: [],
      nodes: [],
      totalCount: 0,
      hasNextPage: false,
    };
  }

  @Query(() => User)
  getUser(
    @Args({ name: 'userId', type: () => String }) userId: string,
  ): Promise<User> {
    // TODO: Userの詳細情報取得

    return Promise.resolve({
      id: userId,
      name: 'shuntaka',
      createdAt: 1665972123,
      modifiedAt: 1665972123,
    });
  }
}

shuntakashuntaka

前項の問題は、N+1にはならないけど、dataloader使えばうまくいきそう(?)

https://recruit.gmo.jp/engineer/jisedai/blog/graphql-dataloader/

https://zenn.dev/tatta/books/5096cb23126e64/viewer/e1ddb1

https://recruit.gmo.jp/engineer/jisedai/blog/graphql-dataloader/

この例も人と記事をJOINすれば、1回で取れるからこの問題解決できなそうだなぁ。
ユーザー詳細のみを取得した場合、と複数概念取得したい場合で柔軟性もたさられるから許容するのがいいのかなぁ、、

N+1になるケース以下の例とかかな。

  1. ユーザー取得
  2. ユーザーIDを元に複数概念を取得
  3. ユーザー分繰り返す(2でN+1発生)

今回のは多くてもリクエスト2回だから今のところは許容するかー。
usersクエリを作るところは、dataloader活用しよう。