Open41

NestJS入門 メモ書き

omisonosoupomisonosoup

プロジェクト作成

$ nest new プロジェクト名

パッケージ管理ツールを選択
yarnだとエラーが起きたのでnpmを選択

omisonosoupomisonosoup

messagesというプロジェクト名で作成した。
コマンドからモジュールを作成できる。

$ nest generate module messages

コントローラーもコマンドで作成できる。
--flatをつけないとmessagesフォルダの下にcontrollersというフォルダを作成した上でファイルを作成する。

$ nest generate controller messages/messages --flat

実際に実行するとこんな感じ

work/nestjs/messages on 🐈 master [?] via ⬢ v14.15.4 
at 19:15:48 ❯ nest generate module messages
CREATE src/messages/messages.module.ts (85 bytes)

work/nestjs/messages on 🐈 master [?] via ⬢ v14.15.4 took 3s 
at 19:15:57 ❯ nest generate controller messages/messages --flat
CREATE src/messages/messages.controller.spec.ts (506 bytes)
CREATE src/messages/messages.controller.ts (105 bytes)
UPDATE src/messages/messages.module.ts (182 bytes)

それぞれ作成、必要なインポートを追記してくれる。

omisonosoupomisonosoup

Controllerにルーティングを書く

messages.controller.ts
import { Controller, Get, Post } from '@nestjs/common';

@Controller('messages')
export class MessagesController {
  @Get()
  listMessages() {}

  @Post()
  createMessages() {}

  @Get('/:id')
  getMessage() {}
}

GET /messages
POST /messages
GET /messages/:id

にそれぞれが対応する。

omisonosoupomisonosoup

リクエストのbodyとparamの取り扱い

messages.controller.ts
import { Body, Param } from '@nestjs/common';

以下のように受け取る

messages.controller.ts
  @Post()
  createMessages(@Body() body: any) {
    console.log(body);
  }

  @Get('/:id')
  getMessage(@Param('id') id: string) {
    console.log(id);
  }
omisonosoupomisonosoup

NestJSではPipeというものを使ってバリデーションなど実装する

main.tsにPipeを追加

main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; // ここと
import { MessagesModule } from './messages/messages.module';

async function bootstrap() {
  const app = await NestFactory.create(MessagesModule);
  app.useGlobalPipes(new ValidationPipe()); // ここ
  await app.listen(3000);
}
bootstrap();

DTOを作る。ここまでの構成は以下のような感じ

src
├── main.ts
└── messages
    ├── dtos
    │   └── create-message.dto.ts
    ├── messages.controller.spec.ts
    ├── messages.controller.ts
    └── messages.module.ts

DTOの中身は以下のような感じ

create-message.dto.ts
import { IsString } from 'class-validator';

export class CreateMessageDto {
  @IsString()
  content: string;
}

npm install class-validator class-transformerを実行しておく
⇨JSONデータをDTOクラスに変換してバリデーションするイメージ?

DTOをコントローラーに設定する

messages.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { CreateMessageDto } from './dtos/create-message.dto'; // ここ

@Controller('messages')
export class MessagesController {
  @Get()
  listMessages() {}

  @Post()
  createMessages(@Body() body: CreateMessageDto) { // ここ
    console.log(body);
  }

  @Get('/:id')
  getMessage(@Param('id') id: string) {
    console.log(id);
  }
}

以下のようなbodyをPOSTすると

{
    "content": 111
}

ちゃんとエラーが返ってくる

{
  "statusCode": 400,
  "message": [
    "content must be a string"
  ],
  "error": "Bad Request"
}
omisonosoupomisonosoup

serviceとrepositoryを作成する。

DBなどへの処理をrepositoryに書いて、
serviceからそれを実行するイメージ

messages.repository.ts
import { readFile, writeFile } from 'fs/promises';

export class MessagesRepository {
  async findOne(id: string) {
    const contents = await readFile('messages.json', 'utf8');
    const messages = JSON.parse(contents);
    return messages[id];
  }
  async findAll() {
    const contents = await readFile('messages.json', 'utf8');
    const messages = JSON.parse(contents);
    return messages;
  }
  async create(content: string) {
    const contents = await readFile('messages.json', 'utf8');
    const messages = JSON.parse(contents);
    const id = Math.floor(Math.random() * 999);
    messages[id] = { id, content };
    await writeFile('messages.json', JSON.stringify(messages));
  }
}

messages.service.ts
import { MessagesRepository } from './messages.repository';

export class MessagesService {
  messagesRepo: MessagesRepository;

  constructor() {
    // 確認のためこのように一旦実装する
    // serviceとrepositoryに依存関係ができてしまうので実際の現場ではこのような書き方はすべきではない
    this.messagesRepo = new MessagesRepository();
  }

  findOne(id: string) {
    return this.messagesRepo.findOne(id);
  }

  findAll() {
    return this.messagesRepo.findAll();
  }

  create(content: string) {
    return this.messagesRepo.create(content);
  }
}

omisonosoupomisonosoup

そしてcontrollerからserviceを呼び出す

messages.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { CreateMessageDto } from './dtos/create-message.dto';
import { MessagesService } from './messages.service';

@Controller('messages')
export class MessagesController {
  messagesService: MessagesService;

  constructor() {
    // 確認のためこのように一旦実装する
    // 依存関係ができてしまうので実際の現場ではこのような書き方はすべきではない
    this.messagesService = new MessagesService();
  }

  @Get()
  listMessages() {
    return this.messagesService.findAll();
  }

  @Post()
  createMessages(@Body() body: CreateMessageDto) {
    return this.messagesService.create(body.content);
  }

  @Get('/:id')
  getMessage(@Param('id') id: string) {
    return this.messagesService.findOne(id);
  }
}
omisonosoupomisonosoup

このままだと例えば存在しないidで取得しようとしてもエラーが発生しない。その場合

messages.controller.ts
.
.
  NotFoundException,
} from '@nestjs/common';

を使って

messages.controller.ts
.
.
  @Get('/:id')
  async getMessage(@Param('id') id: string) {
    const message = await this.messagesService.findOne(id);

    if (!message) {
      throw new NotFoundException('message not found');
    }

    return message;
  }
.
.

とすることでちゃんとエラーを返せる。

HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 68
ETag: W/"44-pb604VmiFWA/9W7nuxCAKFS85bQ"
Date: Tue, 09 Nov 2021 06:06:39 GMT
Connection: close

{
  "statusCode": 404,
  "message": "message not found",
  "error": "Not Found"
}
omisonosoupomisonosoup

controller, service, repository間での依存関係をリファクタリング

まず内部でインスタンスを作るのではなく、
作成済みのインスタンスを受け取るようにする。

messages.service.ts
import { MessagesRepository } from './messages.repository';

export class MessagesService {
  // ここ
  constructor(public messagesRepo: MessagesRepository) {
    this.messagesRepo = messagesRepo;
  }

  findOne(id: string) {
    return this.messagesRepo.findOne(id);
  }

  findAll() {
    return this.messagesRepo.findAll();
  }

  create(content: string) {
    return this.messagesRepo.create(content);
  }
}
messages.controller.ts
.
.
export class MessagesController {
  constructor(public messagesService: MessagesService) {
    this.messagesService = messagesService;
  }
.
.

そしてrepositoryとserviceのクラスにInjectableというデコレーターをつける。
いわゆる依存性の注入。

messages.repository.ts
import { Injectable } from '@nestjs/common';
.
.
@Injectable()
export class MessagesRepository {
messages.repository.ts
import { Injectable } from '@nestjs/common';
.
.
@Injectable()
export class MessagesRepository {

moduleにprovidersを設定することでうまく依存関係を解決してくれる。

messages.module.ts
import { Module } from '@nestjs/common';
import { MessagesController } from './messages.controller';
import { MessagesRepository } from './messages.repository';
import { MessagesService } from './messages.service';

@Module({
  controllers: [MessagesController],
  // ここ
  providers: [MessagesService, MessagesRepository],
})
export class MessagesModule {}
omisonosoupomisonosoup

service, controller, moduleなどコマンドで簡単に作れる、ショートハンドもある

$ nest g service serviceName
$ nest g controller controllerName
$ nest g module moduleName

まずはmoduleを作成して、その後に同名でcontrollerやserviceを作るとうまく階層構造に自動でファイルを作ってくれる。

コマンド実行例
 1001  nest g module computer
 1002  nest g module cpu
 1003  nest g module disk
 1004  nest g module power
 1005  nest g service cpu
 1006  nest g service power
 1007  nest g service disk
 1008  nest g controller computer
階層構造
at 18:40:59 ❯ tree src
src
├── computer
│   ├── computer.controller.spec.ts
│   ├── computer.controller.ts
│   └── computer.module.ts
├── cpu
│   ├── cpu.module.ts
│   ├── cpu.service.spec.ts
│   └── cpu.service.ts
├── disk
│   ├── disk.module.ts
│   ├── disk.service.spec.ts
│   └── disk.service.ts
├── main.ts
└── power
    ├── power.module.ts
    ├── power.service.spec.ts
    └── power.service.ts
omisonosoupomisonosoup

上のような多数のmoduleがある場合
serviceを外部のmoduleから使うためにexportsが必要

power.module.ts
import { Module } from '@nestjs/common';
import { PowerService } from './power.service';

@Module({
  providers: [PowerService],
  exports: [PowerService], // ここ
})
export class PowerModule {}

逆に使う側ではimportsを設定する

cpu.module.ts
import { Module } from '@nestjs/common';
import { PowerModule } from 'src/power/power.module';
import { CpuService } from './cpu.service';

@Module({
  imports: [PowerModule], // ここ
  providers: [CpuService],
})
export class CpuModule {}

importsしたことでservice内で利用できる。

cpu.service.ts
import { Injectable } from '@nestjs/common';
import { PowerService } from 'src/power/power.service';

@Injectable()
export class CpuService {
  constructor(private powerService: PowerService) {}

  compute(a: number, b: number) {
    console.log('Drawing 10 watts of power from Power Service');
    this.powerService.supplyPower(10);
    return a + b;
  }
}

omisonosoupomisonosoup

DBとのコネクションにはTypeORMを使うのが良さそう。
お試しsqlite3

$ npm install @nestjs/typeorm typeorm sqlite3
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // これ
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({ // ここ
      type: 'sqlite',
      database: 'db.sqlite',
      entities: [],
      synchronize: true,
    }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

$ npm run start:devで稼働させると自動でdb.sqliteが作成される。

omisonosoupomisonosoup

Entityを作ってみる。

こんな感じ。クラスにデコレーターをつけていく。

user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  @Column()
  password: string;
}

作ったEntityをmoduleでimportする。

users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])], // ここ
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Appモジュールでもentitiesに設定する。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite',
      entities: [User], // ここ
      synchronize: true,
    }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

※synchronizeオプションはDBのmigrationを自動でするか否かの設定
Entityの変更を即座にDBへ反映してくれる。
開発系などテストをする場合は良いが本番時には注意する。

omisonosoupomisonosoup

ユーザ登録APIを作ってみる。
リクエストbodyのバリデーションのためPipeを使う。

main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes( // ここ
    new ValidationPipe({
      whitelist: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

※whitelist: trueにしておくとユーザが余計なフィールドをリクエストbodyに入れても無視してくれる。
例えばDTOでemailとpasswordだけをこのあと定義するが

requests.http
### Create a new user
POST http://localhost:3000/auth/signup
Content-Type: application/json

{
	"email": "test@test.com",
	"password": "testpass123"
	"admin": true,
}

のようなリクエストをしてもサーバ側では

{ email: 'test@test.com', password: 'testpass123' }

しか受け取らない。

Pipeを使うのにライブラリを入れてくれとエラーが出るので入れる。

$ npm install class-validator class-transformer

DTOの作成

dtos/create-user.dto.ts
import { IsEmail, IsString } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  password: string;
}

controllerにルートを作成

users.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';

@Controller('auth')
export class UsersController {
  @Post('/signup')
  createUser(@Body() body: CreateUserDto) {
    console.log(body);
  }
}
omisonosoupomisonosoup

TypeORMのrepositoryを使っての実際の登録処理を追加する。

users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(@InjectRepository(User) private repo: Repository<User>) {} // ここ

  create(email: string, password: string) { // 登録処理
    /* まずはUser Entityを作る。ここでバリデーションが働く */
    const user = this.repo.create({ email, password });
    /* 直接bodyからobjectを作って入れることもできるが上で作った User Entityを引数に登録する */
    return this.repo.save(user);
  }
}

また一度Entityをcreateしてからsaveすることでentity内で特定のアクション時に処理を実行させることができる。

user.entity.ts
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  AfterInsert,
  AfterRemove,
  AfterUpdate,
} from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  @Column()
  password: string;

  @AfterInsert()  // ここ
  logInsert() {
    console.log('Inserted User with id', this.id);
  }

  @AfterRemove()  // ここ
  logRemove() {
    console.log('Removed User with id', this.id);
  }

  @AfterUpdate()  // ここ
  logUpdate() {
    console.log('Updated User with id', this.id);
  }
}

controllerから呼び出す

users.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UsersService } from './users.service';

@Controller('auth')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Post('/signup')
  createUser(@Body() body: CreateUserDto) {
    this.usersService.create(body.email, body.password);
  }
}

omisonosoupomisonosoup

ユーザデータの取得、更新、削除の処理も作る。

users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(@InjectRepository(User) private repo: Repository<User>) {}

  create(email: string, password: string) {
    const user = this.repo.create({ email, password });
    return this.repo.save(user);
  }

  findOne(id: number) {
    return this.repo.findOne(id);
  }

  find(email: string) {
    return this.repo.find({ email });
  }

  async update(id: number, attrs: Partial<User>) {
    // 一度User Entityを取得
    const user = await this.findOne(id);
    if (!user) {
      throw new Error('user not found');
    }
    // フィールドを更新
    Object.assign(user, attrs);
    // 再度登録
    return this.repo.save(user);
  }

  async remove(id: number) {
    const user = await this.findOne(id);
    if (!user) {
      throw new Error('user not found');
    }
    return this.repo.delete(user);
  }
}
  • 取得系は簡単
  • updateでは一部フィールドだけを更新することを考慮してPartial<User>という風にデータを受け取る。
  • updateでもremoveでも一度User Entityを取得してからそれぞれ処理を行う

それぞれコントローラに設定

users.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Query,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUserDto } from './dtos/update-user.dto';
import { UsersService } from './users.service';

@Controller('auth')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Post('/signup')
  createUser(@Body() body: CreateUserDto) {
    this.usersService.create(body.email, body.password);
  }

  @Get('/:id')
  findUser(@Param('id') id: string) {
    return this.usersService.findOne(parseInt(id));
  }

  @Get()
  findAllUsers(@Query('email') email: string) {
    return this.usersService.find(email);
  }

  @Delete('/:id')
  removeUser(@Param('id') id: string) {
    return this.usersService.remove(parseInt(id));
  }

  @Patch('/:id')
  updateUser(@Param('id') id: string, @Body() body: UpdateUserDto) {
    return this.usersService.update(parseInt(id), body);
  }
}

updateUserではbodyにDTOを設定するけどCreateUserDtoを設定すると全フィールドがないと怒られる。
update用のDTOを作る。

dtos/update-user.dto.ts
import { IsEmail, IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {
  @IsEmail()
  @IsOptional() // これをつけるとなくても良いフィールドになる
  email: string;

  @IsString()
  @IsOptional()  // これをつけるとなくても良いフィールドになる
  password: string;
}

取得、更新、削除の処理で該当IDのユーザが存在しない場合のエラーハンドリングを追加すると以下のような感じ。

users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
.
.
  async update(id: number, attrs: Partial<User>) {
    // 一度User Entityを取得
    const user = await this.findOne(id);
    if (!user) {
      throw new NotFoundException('user not found');
    }
    // フィールドを更新
    Object.assign(user, attrs);
    // 再度登録
    return this.repo.save(user);
  }

  async remove(id: number) {
    const user = await this.findOne(id);
    if (!user) {
      throw new NotFoundException('user not found');
    }
    return this.repo.delete(user);
  }
.
.

取得はコントローラー側で行う

users.controller.ts
.
.
  @Get('/:id')
  async findUser(@Param('id') id: string) {
    const user = await this.usersService.findOne(parseInt(id));
    if (!user) {
      throw new NotFoundException('user not found');
    }
    return user;
  }
.
.
omisonosoupomisonosoup

ユーザデータ取得時にpasswordフィールドが返ってくるのはよろしくない。
entityにデコレーターをつける

users.controller.ts
import { Exclude } from 'class-transformer';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  AfterInsert,
  AfterRemove,
  AfterUpdate,
} from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  @Column()
  @Exclude() // これ
  password: string;

  @AfterInsert()
  logInsert() {
    console.log('Inserted User with id', this.id);
  }

  @AfterRemove()
  logRemove() {
    console.log('Removed User with id', this.id);
  }

  @AfterUpdate()
  logUpdate() {
    console.log('Updated User with id', this.id);
  }
}

controller側にも設定を追加

users.controller.ts
import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Delete,
  Get,
  NotFoundException,
  Param,
  Patch,
  Post,
  Query,
  UseInterceptors,
} from '@nestjs/common';
.
.
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('/:id')
  async findUser(@Param('id') id: string) {
    const user = await this.usersService.findOne(parseInt(id));
    if (!user) {
      throw new NotFoundException('user not found');
    }
    return user;
  }
.
.
omisonosoupomisonosoup

上のはすごく簡易的。
もっとちゃんとデータをシリアライズするには自分でカスタマイズして作る。

interceptors/serialize.interceptor.ts
import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

export class SerializeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
    // リクエストハンドラーによってリクエストがハンドルされる前に何かする処理
    console.log("I'm running before the handler", context);

    return handler.handle().pipe(
      map((data: any) => {
        // レスポンスが返される前に何かする処理
        console.log("I'm running before response is sent out", data);
      }),
    );
  }
}

上のようなファイルを作って、controllerで読み込む。

users.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  NotFoundException,
  Param,
  Patch,
  Post,
  Query,
  UseInterceptors,
} from '@nestjs/common';
import { SerializeInterceptor } from 'src/interceptors/serialize.interceptor';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUserDto } from './dtos/update-user.dto';
import { UsersService } from './users.service';

@Controller('auth')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Post('/signup')
  createUser(@Body() body: CreateUserDto) {
    this.usersService.create(body.email, body.password);
  }

  @UseInterceptors(SerializeInterceptor) // ここ
  @Get('/:id')
  async findUser(@Param('id') id: string) {
    const user = await this.usersService.findOne(parseInt(id));
    if (!user) {
      throw new NotFoundException('user not found');
    }
    return user;
  }
.
.

以下のようにDTOを作って公開したいフィールドにexposeをつける

dtos/user.dto.ts
import { Expose } from 'class-transformer';

export class UserDto {
  @Expose()
  id: number;

  @Expose()
  email: string;
}

シリアライザーではdataをUserDtoクラスに変換して返すようにする。
excludeExtraneousValuesをtrueにすることでDTOクラスのexposeの設定に従う。

interceptors/serialize.interceptor.ts
import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';
import { UserDto } from 'src/users/dtos/user.dto';

export class SerializeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
    // リクエストハンドラーによってリクエストがハンドルされる前に何かする処理
    console.log("I'm running before the handler", context);

    return handler.handle().pipe(
      map((data: any) => {
        // レスポンスが返される前に何かする処理
        console.log("I'm running before response is sent out", data);
        // データをUserDtoの公開設定に従って返す
        return plainToClass(UserDto, data, {
          excludeExtraneousValues: true,
        });
      }),
    );
  }
}

constructorでDTOを受け取るようにすると汎用性が増す

serialize.interceptor.ts
import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

export class SerializeInterceptor implements NestInterceptor {
  constructor(private dto: any) {}

  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
    // リクエストハンドラーによってリクエストがハンドルされる前に何かする処理
    console.log("I'm running before the handler", context);

    return handler.handle().pipe(
      map((data: any) => {
        // レスポンスが返される前に何かする処理
        console.log("I'm running before response is sent out", data);
        // データをUserDtoの公開設定に従って返す
        return plainToClass(this.dto, data, {
          excludeExtraneousValues: true,
        });
      }),
    );
  }
}

使う側はこんな感じ

users.controller.ts
.
.
  @UseInterceptors(new SerializeInterceptor(UserDto))
.
.

先にSerializeのデコレーターを作っておいてcontroller側ではそれを設定するようにするとBetter

serialize.interceptor.ts
.
.
export function Serialize(dto: any) {
  return UseInterceptors(new SerializeInterceptor(dto));
}
.
.
users.controller.ts
.
.
  @Serialize(UserDto)
.
.

なおcontrollerのトップに設置すると全てのUserデータ返り値に適用できる

users.controller.ts
.
.
@Controller('auth')
@Serialize(UserDto)  // ここ
export class UsersController {
  constructor(private usersService: UsersService) {}
.
.
omisonosoupomisonosoup

Serializeをより型安全にするには以下のようにする。

serialize.interceptor.ts
.
.
interface ClassConstructor {
  new (...args: any[]): {};
}

export function Serialize(dto: ClassConstructor) { // 何かしらのクラス
  return UseInterceptors(new SerializeInterceptor(dto));
}
.
.
omisonosoupomisonosoup

認証機能をつけてみる

usersの階層にauth.service.tsを追加

auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from './users.service';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
}

users.module.tsのプロバイダーに追加

users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { AuthService } from './auth.service';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService, AuthService],
})
export class UsersModule {}
omisonosoupomisonosoup

登録とサインイン処理を作るとこんな感じ

auth.service.ts
import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { randomBytes, scrypt as _scrypt } from 'crypto';
import { promisify } from 'util';

const scrypt = promisify(_scrypt);

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async signup(email: string, password: string) {
    // 既存ユーザがいるかを探す
    const users = await this.usersService.find(email);
    if (users.length) {
      throw new BadRequestException('email in use');
    }

    // パスワードハッシュ化
    const salt = randomBytes(8).toString('hex');
    const hash = (await scrypt(password, salt, 32)) as Buffer;
    const result = salt + '.' + hash.toString('hex');

    // ユーザを作成して登録
    const user = await this.usersService.create(email, result);

    // 作成したユーザを返す
    return user;
  }

  async signin(email: string, password: string) {
    // ユーザがいるか確認する
    const [user] = await this.usersService.find(email);
    if (!user) {
      throw new NotFoundException('user not found');
    }

    // ハッシュ化したパスワードで一致を確認する
    const [salt, storedHash] = user.password.split('.');
    const hash = (await scrypt(password, salt, 32)) as Buffer;
    if (storedHash === hash.toString('hex')) {
      // 正しければユーザを返す
      return user;
    } else {
      throw new BadRequestException('bad password');
    }
  }
}

コントローラに追加する

users.controller.ts
@Controller('auth')
@Serialize(UserDto)
export class UsersController {
  constructor(
    private usersService: UsersService,
    private authService: AuthService,
  ) {}

  @Post('/signup')
  createUser(@Body() body: CreateUserDto) {
    return this.authService.signup(body.email, body.password);
  }

  @Post('/signin')
  signin(@Body() body: CreateUserDto) {
    return this.authService.signin(body.email, body.password);
  }
omisonosoupomisonosoup

Cookieセッションを実装する。

$ npm install cookie-session @types/cookie-session 

ミドルウェアを追加

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

// Nestと相性が悪い?ためrequireで使用する
const cookieSession = require('cookie-session');

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // ミドルウェアをここで追加
  app.use(
    cookieSession({
      keys: ['asdfasdf'],
    }),
  );
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();
omisonosoupomisonosoup

認証時にセッションにユーザIDをセットする

users.controller.ts
  @Post('/signup')
  async createUser(@Body() body: CreateUserDto, @Session() session: any) {
    const user = await this.authService.signup(body.email, body.password);
    session.userId = user.id;
    return user;
  }

  @Post('/signin')
  async signin(@Body() body: CreateUserDto, @Session() session: any) {
    const user = await this.authService.signin(body.email, body.password);
    session.userId = user.id;
    return user;
  }
omisonosoupomisonosoup

セッションを使ってユーザ情報取得とサインアウト処理

users.controller.ts
  @Post('/signout')
  signOut(@Session() session: any) {
    session.userId = null;
  }

  @Get('/whoami')
  whoAmI(@Session() session: any) {
    return this.usersService.findOne(session.userId);
  }
omisonosoupomisonosoup

デコレータとインターセプターを使ってユーザ情報をリクエストに含めるようにする。

users.controller.ts
  @Get('/whoami')
  whoAmI(@CurrentUser() user: User) {
    return user;
  }

  // @Get('/whoami')
  // whoAmI(@Session() session: any) {
  //   return this.usersService.findOne(session.userId);
  // }

デコレータとインターセプターを作る

decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  // dataはCurrentUserの引数が入る。不要なのでneverにしておく
  (data: never, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest();
    // interceptorでユーザを取得してリクエストにセットしている
    return request.currentUser;
  },
);
interceptors/current-user.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { UsersService } from '../users.service';

@Injectable()
export class CurrentUserInterceptor implements NestInterceptor {
  constructor(private usersService: UsersService) {}

  async intercept(context: ExecutionContext, handler: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const { userId } = request.session || {};

    if (userId) {
      const user = await this.usersService.findOne(userId);
      request.currentUser = user;
    }

    return handler.handle();
  }
}

providerに追加

users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { AuthService } from './auth.service';
import { CurrentUserInterceptor } from './interceptors/current-user.interceptor';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService, AuthService, CurrentUserInterceptor], // ここ
})
export class UsersModule {}

各種コントローラーでinterceptorが機能する

users.controller.ts
@UseInterceptors(CurrentUserInterceptor) // ここ
export class UsersController {
  constructor(
    private usersService: UsersService,
    private authService: AuthService,
  ) {}

グローバルに設定したい場合は以下のようにする

users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';
import { AuthService } from './auth.service';
import { CurrentUserInterceptor } from './interceptors/current-user.interceptor';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [
    UsersService,
    AuthService,
    // ここ
    {
      provide: APP_INTERCEPTOR,
      useClass: CurrentUserInterceptor,
    },
  ],
})
export class UsersModule {}
omisonosoupomisonosoup

認証済みの場合のみ許可したいルートを設定する場合

guardを作る

guards/auth.guard.ts
import { CanActivate, ExecutionContext } from '@nestjs/common';

export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();

    return request.session.userId;
  }
}

作ったguardをコントローラに設定

users.controller.ts
  @Get('/whoami')
  @UseGuards(AuthGuard)
  whoAmI(@CurrentUser() user: User) {
    return user;
  }
omisonosoupomisonosoup

テストファイルの作成

こんな感じ

auth.service.spec.ts
import { Test } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { UsersService } from './users.service';

it('can create an instance of auth service', async () => {

  // Partialを使って依存するサービスを型指定してフェイクのサービスを作成する
  const fakeUsersService: Partial<UsersService> = {
    find: () => Promise.resolve([]),
    create: (email: string, password: string) =>
      Promise.resolve({ id: 1, email, password } as User),
  };

  const module = await Test.createTestingModule({
    providers: [
      AuthService,
      // UsersServiceを使うときは、fakeUserServiceを使ってね!という設定
      {
        provide: UsersService,
        useValue: fakeUsersService,
      },
    ],
  }).compile();

  const service = module.get(AuthService);
  expect(service).toBeDefined();
});
omisonosoupomisonosoup

より実践的にdescribeとbeforeEachを使って書く

auth.service.spec.ts
import { Test } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { UsersService } from './users.service';

describe('AuthService', () => {
  let service: AuthService;

  beforeEach(async () => {
    const fakeUsersService: Partial<UsersService> = {
      find: () => Promise.resolve([]),
      create: (email: string, password: string) =>
        Promise.resolve({ id: 1, email, password } as User),
    };
    const module = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UsersService,
          useValue: fakeUsersService,
        },
      ],
    }).compile();
    service = module.get(AuthService);
  });

  it('can create an instance of auth service', async () => {
    expect(service).toBeDefined();
  });
});
omisonosoupomisonosoup

登録処理のテスト

auth.service.spec.ts
  it('creates a new user with a salted and hashed password', async () => {
    const user = await service.signup('asdf@asdf.com', 'asdf');

    // ハッシュ化されるので異なる
    expect(user.password).not.toEqual('asdf');
    // saltとhashは.で区切る仕様
    const [salt, hash] = user.password.split('.');
    expect(salt).toBeDefined();
    expect(hash).toBeDefined();
  });
omisonosoupomisonosoup

使用済みのメールアドレスがある場合は登録に失敗することをテストする場合。
モック化したfakeUsersServiceをいじりたい。
宣言部分で外部に出しておくように変更

auth.service.spec.ts
  let fakeUsersService: Partial<UsersService>;

  beforeEach(async () => {
    fakeUsersService = {
      find: () => Promise.resolve([]),
      create: (email: string, password: string) =>
        Promise.resolve({ id: 1, email, password } as User),
    };

テスト部分ではfindを定義して既にユーザがいることを想定させる。

auth.service.spec.ts
  it('throws an error if user signs up with email that is in use email', (done) => {
    fakeUsersService.find = () =>
      Promise.resolve([{ id: 1, email: 'a', password: '1' } as User]);
    service.signup('asdf@asdf.com', 'asdf').catch(() => done());
  });
omisonosoupomisonosoup

同様の感じでサインインもテスト。
一つ目はfindのデフォルトが何も返ってこないように設定しているのでそのまま書ける。

auth.service.spec.ts
  it('既存ユーザがいる場合サインイン失敗', (done) => {
    service.signin('asdf@asdf.com', 'asdf').catch(() => {
      done();
    });
  });

  it('パスワードが違う場合にサインイン失敗', (done) => {
    fakeUsersService.find = () =>
      Promise.resolve([{ email: 'asdf@asdf.com', password: 'asdf' } as User]);
    service.signin('wase@wase.com', 'wase').catch(() => done());
  });

  it('パスワードが正しい場合にサインイン成功', async () => {
    // 一度ハッシュ化したパスワードを作成して確認しておく
    // const user = await service.signup('asdf@asdf.com', 'asdf');
    // console.log(user);
    fakeUsersService.find = () =>
      Promise.resolve([
        {
          email: 'asdf@asdf.com',
          password:
            '423caf1795c6a034.a7b5f4027d502936775218034f3e884941a58336fc139b14f0e54a84e135f1c6',
        } as User,
      ]);
    const user = await service.signin('asdf@asdf.com', 'asdf');
    expect(user).toBeDefined();
  });
omisonosoupomisonosoup

これくらいの規模だとまあ大丈夫だけどさらに複雑になったらモックも難しくなる。
より簡略化したcreate, findを定義して使うように修正する。

auth.service.spec.ts
.
.
 beforeEach(async () => {
    // ユーザが格納される擬似ストレージ
    const users: User[] = [];
    fakeUsersService = {
      find: (email: string) => {
        const filteredUsers = users.filter((user) => user.email === email);
        return Promise.resolve(filteredUsers);
      },
      create: (email: string, password: string) => {
        const user = {
          id: Math.floor(Math.random() * 999999),
          email,
          password,
        } as User;
        users.push(user);
        return Promise.resolve(user);
      },
    };
.
.

テストが簡略化できる

auth.service.spec.ts
  // before
  it('パスワードが正しい場合にサインイン成功', async () => {
    // 一度ハッシュ化したパスワードを作成して確認しておく
    // const user = await service.signup('asdf@asdf.com', 'asdf');
    // console.log(user);
    fakeUsersService.find = () =>
      Promise.resolve([
        {
          email: 'asdf@asdf.com',
          password:
            '423caf1795c6a034.a7b5f4027d502936775218034f3e884941a58336fc139b14f0e54a84e135f1c6',
        } as User,
      ]);
    const user = await service.signin('asdf@asdf.com', 'asdf');
    expect(user).toBeDefined();
  });

  // after
  it('パスワードが正しい場合にサインイン成功', async () => {
    await service.signup('asdf@asdf.com', 'asdf');
    const user = await service.signin('asdf@asdf.com', 'asdf');
    expect(user).toBeDefined();
  });
omisonosoupomisonosoup

他のテストも以下のようにより自然なテストに修正できる。

auth.service.spec.ts
  // before
  it('throws an error if user signs up with email that is in use email', (done) => {
    fakeUsersService.find = () =>
      Promise.resolve([{ id: 1, email: 'a', password: '1' } as User]);
    service.signup('asdf@asdf.com', 'asdf').catch(() => done());
  });

  // after
  it('throws an error if user signs up with email that is in use email', (done) => {
    service.signup('asdf@asdf.com', 'asdf').then(() => {
      service.signup('asdf@asdf.com', 'asdf').catch(() => done());
    });
  });
auth.service.spec.ts
  it('パスワードが違う場合にサインイン失敗', (done) => {
    fakeUsersService.find = () =>
      Promise.resolve([{ email: 'asdf@asdf.com', password: 'asdf' } as User]);
    service.signin('wase@wase.com', 'wase').catch(() => done());
  });

  // after
  it('パスワードが違う場合にサインイン失敗', (done) => {
    service.signup('wase@wase.com', 'wase').then(() => {
      service.signin('wase@wase.com', 'wasee').catch(() => done());
    });
  });
omisonosoupomisonosoup

コントローラのテスト

雛形は自動生成される。
コントローラーのconstructorを見て、関係するサービスをモックする。

users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('UsersController', () => {
  let controller: UsersController;
  let fakeUsersService: Partial<UsersService>;
  let fakeAuthService: Partial<AuthService>;

  beforeEach(async () => {
    fakeUsersService = {
      findOne: () => {},
      find: () => {},
      remove: () => {},
      update: () => {},
    };
    fakeAuthService = {
      signup: () => {},
      signin: () => {},
    };
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
    }).compile();

    controller = module.get<UsersController>(UsersController);
  });
.
.

それぞれの処理はvscodeでホバーするとtypeエラーの内容が出てくるので従って、ダミーの処理を書いていく。

omisonosoupomisonosoup

e2eテストの例

雛形は自動生成されているのでコピペして調整する

auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('Authentication System (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('handles a signup request', () => {
    const email = 'asdfdafad@test.com';
    return request(app.getHttpServer())
      .post('/auth/signup')
      .send({ email, password: 'dafadf' })
      .expect(201)
      .then((res) => {
        const { id, email } = res.body;
        expect(id).toBeDefined();
        expect(email).toEqual(email);
      });
  });
});

普通にテスト実施するとエラーが起きる。

[Nest] 91194  - 2021/11/28 20:46:13   ERROR [ExceptionsHandler] Cannot set property 'userId' of undefined
TypeError: Cannot set property 'userId' of undefined
omisonosoupomisonosoup

ミドルウェアが不足していることが原因。
別途切り分けてテスト側でも呼び出せるようにする。

main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

// Nestと相性が悪い?ためrequireで使用する
const cookieSession = require('cookie-session');

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // app.use~の部分
  app.use(
    cookieSession({
      keys: ['asdfasdf'],
    }),
  );
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();
setup-app.ts
import { INestApplication, ValidationPipe } from '@nestjs/common';
// Nestと相性が悪い?ためrequireで使用する
const cookieSession = require('cookie-session');

export const setupApp = (app: INestApplication) => {
  // ミドルウェアをここで追加
  app.use(
    cookieSession({
      keys: ['asdfasdf'],
    }),
  );
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),
  );
};

んで、使う。

main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { setupApp } from './setup-app';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  setupApp(app);
  await app.listen(3000);
}
bootstrap();

テスト側でも呼ぶようにする。

auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { setupApp } from '../src/setup-app';

describe('Authentication System (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    // ここ
    setupApp(app);
    await app.init();
  });

  it('handles a signup request', () => {
    const email = 'asdfdafad@test.com';
    return request(app.getHttpServer())
      .post('/auth/signup')
      .send({ email, password: 'dafadf' })
      .expect(201)
      .then((res) => {
        const { id, email } = res.body;
        expect(id).toBeDefined();
        expect(email).toEqual(email);
      });
  });
});

メールアドレスを変えてテストを実行するとパスする。

omisonosoupomisonosoup

もう一つの方法。
app.module.tsにミドルウェアを含ませる。

app.module.ts
import { MiddlewareConsumer, Module, ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';
import { User } from './users/user.entity';
import { Report } from './reports/report.entity';
const cookieSession = require('cookie-session');

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite',
      entities: [User, Report],
      synchronize: true,
    }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [
    AppService,
    // ここでバリデーションパイプ
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true,
      }),
    },
  ],
})
export class AppModule {
  // ここでクッキーセッション
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(
        cookieSession({
          keys: ['asdfasdf'],
        }),
      )
      .forRoutes('*');
  }
}
omisonosoupomisonosoup

NestJSで環境変数を扱う

$ npm i @nestjs/config

設定

app.module.ts
import { MiddlewareConsumer, Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_PIPE } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';
import { User } from './users/user.entity';
import { Report } from './reports/report.entity';
const cookieSession = require('cookie-session');

@Module({
  imports: [
    // ここ
    ConfigModule.forRoot({
      // グローバルにする設定
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
    // 宣言後、database名などに環境変数を使いたいがここでprocess.env.DB_NAMEなどとしてもundefinedになる
    // TypeOrmModule.forRoot({
    //   type: 'sqlite',
    //   database: 'db.sqlite',
    //   entities: [User, Report],
    //   synchronize: true,
    // }),
    // TypeOrmの設定を以下のように変える
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          type: 'sqlite',
          database: config.get<string>('DB_NAME'),
          synchronize: true,
          entities: [User, Report],
        };
      },
    }),
.
.
omisonosoupomisonosoup

このままだとNODE_ENVの値はundefinedになる。
cross-envを利用する。

$ npm install cross-env
$ npm

各scriptsを書き換えるとNODE_ENVに環境の値が入るようになる。

package.json
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "cross-env NODE_ENV=development nest start",
    "start:dev": "cross-env NODE_ENV=development nest start --watch",
    "start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "cross-env NODE_ENV=test jest",
    "test:watch": "cross-env NODE_ENV=test jest --watch --maxWorkers=1",
    "test:cov": "cross-env NODE_ENV=test jest --coverage",
    "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json"
  },
omisonosoupomisonosoup

環境変数でテスト用のDBを用意できるようになったので、e2eテストのたびにDBをワイプして、何度も登録に関するテストが通るようにしたい。現状、同じメールアドレスで登録テストを実施してエラーになってしまう。

設定ファイルにsetupファイルに関する記述を追加する。

jest-e2e.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  // ここ エラーが出る場合はrelativeパスに変更
  "setupFilesAfterEnv": ["<rootDir>/setup.ts>"]
}

このrootDirはtestディレクトリを示す。
setup.tsを作成

test/setup.ts
import { rm } from 'fs/promises';
import { join } from 'path';
import { getConnection } from 'typeorm';

// sqliteファイルを削除
global.beforeEach(async () => {
  try {
    await rm(join(__dirname, '..', 'test.sqlite'));
  } catch (err) {}
});

// テストが終わったら接続を切る。切らないと削除できない。
global.afterEach(async () => {
  const conn = await getConnection();
  await conn.close();
});
omisonosoupomisonosoup

テスト追加

auth.e2e-spec.ts
  it('サインアップしてユーザ情報取得', async () => {
    const email = 'first@test.com';

    const res = await request(app.getHttpServer())
      .post('/auth/signup')
      .send({ email, password: 'dafadf' })
      .expect(201);

    const cookie = res.get('Set-Cookie');

    const { body } = await request(app.getHttpServer())
      .get('/auth/whoami')
      .set('Cookie', cookie)
      .expect(200);

    expect(body.email).toEqual(email);
  });