🏗️

NestJSでREST API

2023/05/29に公開

環境構築

Node.jsが利用できる環境であれば特に環境は指定しませんが, Dockerの場合は下記のような環境を使用します。

docker run -u node -it -w /home/node/workspace -v $HOME/nestjs:/home/node/workspace node sh -c "yarn global add @nestjs/cli && /home/node/.yarn/bin/nest new ."

ここまででグローバル環境に@nestjs/cliをインストールし, プロジェクトのルートでnest new .を実行します。途中, 使用するパッケージマネージャを聞かれるので適当なものを選択してください。

Fastifyを使う

NestJSはデフォルトではExpressを使用しますが, パフォーマンスの面からFastifyへ変更していきます。

yarn add @nestjs/platform-fastify

インストール後はsrc/main.tsを変更します。

src/main.ts
    // 省略
-   const app = await NestFactory.create(AppModule);
+   const app = await NestFactory.create<NestFastifyApplication>(
+     AppModule,
+     new FastifyAdapter(),
+   );
    // 省略
-   await app.listen(3000);
+   await app.listen(3000, '0.0.0.0');
  }
  bootstrap();

Prismaを使う

yarn add --dev prisma
yarn prisma init

Prismaのプロジェクトを初期化すると.envprisma/schema.prismaが生成されるため適宜編集していきます。DATABASE_URLPrisma公式で扱われている書式で使用することができます。prisma/schema.prismaではデータベースのテーブルを記述することができます。ここでは簡単にUserテーブルをひとつ定義することにします。

prisma/schema.prisma
  // 省略
+   model User {
+     id Int @id @default(autoincrement())
+     username String
+   }
  }

スキーマの定義の後は初回マイグレーションを実行します。

yarn prisma generate
yarn prisma migrate dev --name init

yarn prisma generateのみでもTypeScriptの型を使用することができます。

NestJSのリソースを追加する

NestJSでは簡単にCRUDを含むResourcesを生成することができます。

nest generate resource user

上記はnest g res userをエイリアスとして使用することができます。実行するとuserについて, REST APIやGraphQLといったレイヤを選択することができます。REST APIを選択すると次のファイルが生成されます。

  • DTO(Data Transfer Object)
  • Controller
  • Service
  • Entity

API仕様書を作る

Fastifyを使用する場合は@fastify/staticも同時にインストールします。

yarn add @nestjs/swagger @fastify/static

main.tsを次のように変更すると, OpenAPIのドキュメントを使用することができます。

src/main.ts
    // 省略
+   const config = new DocumentBuilder()
+     .setTitle('User example')
+     .setDescription('The users API description')
+     .setVersion('1.0')
+     .build();
+   const document = SwaggerModule.createDocument(app, config);
+   SwaggerModule.setup('api', app, document);

    await app.listen(3000, '0.0.0.0');
  }
  bootstrap();

yarn startでサーバを起動しlocalhost:3000/apiを開くとGUIからAPIを使用することができます。同時に, api-yamlapi-jsonでそれぞれyamlとjsonを取得することができます。

ドキュメントを作る

デコレータを用いてOpenAPIでの例や型を付けていきます。

src/user/dto/create-user.dto.ts
// 省略
+ export class CreateUserDto {
+   @ApiProperty({ example: 'john Doe' })
+   username: string;
+ }

DTOとEntityは上記のように変更することで注釈と型をSwagger UIから確認することができます。

src/user/user.controller.ts
  // 省略
+ @ApiTags('user')
  @Controller('user')
  export class UserController {
  // 省略

また, ControllerにApiTagsデコレータを使用することでController全体をuserとしてまとめることができます。

DI(Dependency Injection)

データベースを扱うクラスを抽象化し, テスト可能なServiceを作ります。はじめに次のファイルを作成します。

src/repository/users.repository.ts
import { User } from 'src/user/entities/user.entity';

export abstract class UsersRepository {
  abstract create(user: User): Promise<User>;
  abstract readAll(): Promise<User[]>;
  abstract readById(id: number): Promise<User>;
  abstract update(user: User): Promise<User>;
  abstract delete(id: number): Promise<User>;
}

export class UserRepositoryError extends Error {
  readonly type: UserRepositoryErrorType;
  constructor(type: UserRepositoryErrorType) {
    super();
    this.type = type;
  }
}

export type UserRepositoryErrorType = 'RECORD_NOT_FOUND' | 'FATAL';

抽象クラスについて注意すべき点はDIのためにinterfaceではなくabstract classを使用します。実装は次のようになります。

src/repository/users.db.ts
import { User } from 'src/user/entities/user.entity';
import { UserRepositoryError, UsersRepository } from './users.repository';
import { PrismaClient } from '@prisma/client';

export class UsersDb extends UsersRepository {
  async create(user: User): Promise<User> {
    try {
      const prisma = new PrismaClient();
      return prisma.user.create({
        data: { id: user.id, username: user.username },
      });
    } catch (error) {
      throw new UserRepositoryError('FATAL');
    }
  }
  // 省略
}

次に, src/user/user.module.tsでDIを実装します。

src/user/user.module.ts
  // 省略
  @Module({
    controllers: [UserController],
-   providers: [UserService]
+   providers: [UserService, { provide: UsersRepository, useClass: UsersDb }],
  })
  export class UserModule {}

src/user/user.service.tsでは次のように抽象クラスを使用することができます。

src/user/user.service.ts
  @Injectable()
  export class UserService {
+   constructor(
+     @Inject(UsersRepository) private readonly usersRepository: UsersRepository,
+   ) {}

-   create(createUserDto: CreateUserDto) {
+   async create(createUserDto: CreateUserDto): Promise<User> {
    // 省略
  }

参考文献

Discussion