コンテンツにスキップ

NestJS を使う

NestJS は Web フレームワークである。

インストール

公式の方法は以下。

First steps | NestJS - A progressive Node.js framework

1
2
$ npm i -g @nestjs/cli
$ nest new project-name

nest コマンドをインストールしない場合は以下。

1
2
3
4
$ mkdir {プロジェクト名}
$ cd {プロジェクト名}
$ npm i @nestjs/cli --no-save
$ npx nest new {プロジェクト名} --directory=./

Controller, Service, Module

Controller はリクエストを受信して、クライアントにレスポンスを返す。

Service はビジネスロジックを定義する。

Module は Controller と Service を紐づけて NestJS に登録する。

Todo API を作る

リソースを作る

npx nest g resourceControllerServiceModuleを作ってくれる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ npx nest g resource
? What name would you like to use for this resource (plural, e.g., "users")? todos
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/todos/todos.controller.spec.ts (566 bytes)
CREATE src/todos/todos.controller.ts (894 bytes)
CREATE src/todos/todos.module.ts (248 bytes)
CREATE src/todos/todos.service.spec.ts (453 bytes)
CREATE src/todos/todos.service.ts (609 bytes)
CREATE src/todos/dto/create-todo.dto.ts (30 bytes)
CREATE src/todos/dto/update-todo.dto.ts (169 bytes)
CREATE src/todos/entities/todo.entity.ts (21 bytes)
UPDATE package.json (2023 bytes)
UPDATE src/app.module.ts (312 bytes) Packages installed successfully.
src/todos/entities/todo.entity.ts
1
2
3
4
5
export class Todo {
  id: string;
  name: string;
  done: boolean;
}
src/todos/todos.service.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { Injectable } from "@nestjs/common";
import { CreateTodoDto } from "./dto/create-todo.dto";
import { UpdateTodoDto } from "./dto/update-todo.dto";
import { Todo } from "./entities/todo.entity";

@Injectable()
export class TodosService {
  private todos: Todo[] = [];

  constructor() {
    this.create({ name: "Sample #1", done: false });
    this.create({ name: "Sample #2", done: false });
    this.create({ name: "Sample #3", done: true });
  }

  create(createTodoDto: CreateTodoDto): Todo {
    const todo: Todo = {
      id: crypto.randomUUID(),
      name: createTodoDto.name,
      done: createTodoDto.done,
    };
    this.todos = [...this.todos, todo];
    return todo;
  }

  findAll(): Todo[] {
    return this.todos;
  }

  findOne(id: string): Todo {
    return this.todos.find((t) => t.id === id);
  }

  update(id: string, updateTodoDto: UpdateTodoDto): Todo {
    const todo = this.findOne(id);
    if (updateTodoDto.name !== undefined) {
      todo.name = updateTodoDto.name;
    }
    if (updateTodoDto.done !== undefined) {
      todo.done = updateTodoDto.done;
    }
    return todo;
  }

  remove(id: string): void {
    this.todos = this.todos.filter((t) => t.id !== id);
  }
}
src/todos/todos.controller.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
} from "@nestjs/common";
import { CreateTodoDto } from "./dto/create-todo.dto";
import { UpdateTodoDto } from "./dto/update-todo.dto";
import { TodosService } from "./todos.service";

@Controller("todos")
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  @Post()
  create(@Body() createTodoDto: CreateTodoDto) {
    return this.todosService.create(createTodoDto);
  }

  @Get()
  findAll() {
    return this.todosService.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return this.todosService.findOne(id);
  }

  @Patch(":id")
  update(@Param("id") id: string, @Body() updateTodoDto: UpdateTodoDto) {
    return this.todosService.update(id, updateTodoDto);
  }

  @Delete(":id")
  @HttpCode(204)
  remove(@Param("id") id: string) {
    this.todosService.remove(id);
  }
}
src/todos/dto/create-todo.dto.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { IsBoolean, IsNotEmpty, IsString } from "class-validator";

export class CreateTodoDto {
  @IsNotEmpty()
  @IsString()
  name: string;

  @IsNotEmpty()
  @IsBoolean()
  done: boolean;
}

リクエストのバリデーションを行う

Validation | NestJS - A progressive Node.js framework

1
$ npm i class-validator class-transformer

DTO にデコレータをつける。

src/todos/dto/create-todo.dto.ts
1
2
3
4
5
6
7
8
import { IsNotEmpty } from "class-validator";

export class CreateTodoDto {
  @IsNotEmpty()
  name: string;
  @IsNotEmpty()
  done: boolean;
}
src/main.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

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

エラーハンドリングを行う

Exception filters | NestJS - A progressive Node.js framework

存在しない ID へのリクエストなど、エラーをハンドリングできるようにする。

バックエンド Web アプリケーションではエラーが起きた場合、最終的に HTTP ステータスコードをユーザーに返すことになる。

しかし Service 層は抽象的であるべきで、 HTTP のことを意識したくないので、HTTP ステータスコードについて書くのは最も外界に近い Controller 層が望ましいと思う。

よって Service 層でエラーが起きた場合はErrorを throw することにして、Controller 層は受けたErrorを元に HttpException を throw することにする。

src/todos/errors/todo-not-found-error.ts
1
export class TodoNotFoundError extends Error {}
src/todos/todos.service.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import { Injectable } from "@nestjs/common";
import { CreateTodoDto } from "./dto/create-todo.dto";
import { UpdateTodoDto } from "./dto/update-todo.dto";
import { Todo } from "./entities/todo.entity";
import { TodoNotFoundError } from "./errors/todo-not-found-error";

@Injectable()
export class TodosService {
  private todos: Todo[] = [];

  constructor() {
    console.log("contructor");
    this.create({ name: "Sample #1", done: false });
    this.create({ name: "Sample #2", done: false });
    this.create({ name: "Sample #3", done: true });
  }

  create(createTodoDto: CreateTodoDto): Todo {
    const todo: Todo = {
      id: crypto.randomUUID(),
      name: createTodoDto.name,
      done: createTodoDto.done,
    };
    this.todos = [...this.todos, todo];
    return todo;
  }

  findAll(): Todo[] {
    return this.todos;
  }

  findOne(id: string): Todo {
    if (!this.exists(id)) {
      throw new TodoNotFoundError();
    }
    return this.todos.find((t) => t.id === id);
  }

  update(id: string, updateTodoDto: UpdateTodoDto): Todo {
    const todo = this.findOne(id);
    if (updateTodoDto.name !== undefined) {
      todo.name = updateTodoDto.name;
    }
    if (updateTodoDto.done !== undefined) {
      todo.done = updateTodoDto.done;
    }
    return todo;
  }

  remove(id: string): void {
    if (!this.exists(id)) {
      throw new TodoNotFoundError();
    }
    this.todos = this.todos.filter((t) => t.id !== id);
  }

  private exists(id: string): boolean {
    return this.todos.find((t) => t.id === id) !== undefined;
  }
}
src/todos/todos.controller.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpException,
  Param,
  Patch,
  Post,
} from "@nestjs/common";
import { CreateTodoDto } from "./dto/create-todo.dto";
import { UpdateTodoDto } from "./dto/update-todo.dto";
import { TodoNotFoundError } from "./errors/todo-not-found-error";
import { TodosService } from "./todos.service";

@Controller("todos")
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  @Post()
  create(@Body() createTodoDto: CreateTodoDto) {
    return this.todosService.create(createTodoDto);
  }

  @Get()
  findAll() {
    return this.todosService.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    try {
      return this.todosService.findOne(id);
    } catch (error: any) {
      this.catchError(error);
    }
  }

  @Patch(":id")
  update(@Param("id") id: string, @Body() updateTodoDto: UpdateTodoDto) {
    try {
      return this.todosService.update(id, updateTodoDto);
    } catch (error: any) {
      this.catchError(error);
    }
  }

  @Delete(":id")
  @HttpCode(204)
  remove(@Param("id") id: string) {
    try {
      this.todosService.remove(id);
    } catch (error: any) {
      this.catchError(error);
    }
  }

  private catchError(error: any) {
    if (error instanceof TodoNotFoundError) {
      throw new HttpException("Not found", 404);
    } else {
      throw new HttpException("Internal server error", 500);
    }
  }
}

TypeORM で MySQL

Database | NestJS - A progressive Node.js framework

インターセプター

Interceptors | NestJS - A progressive Node.js framework

CORS

app.enableCors()を使う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

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

他のリソースの Service を inject する

TodosService で UsersService を参照する例を示す。

src/users/users.module.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./entities/user.entity";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // ★
})
export class UsersModule {}
src/todos/todos.module.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UsersModule } from "src/users/users.module";
import { Todo } from "./entities/todo.entity";
import { TodosController } from "./todos.controller";
import { TodosService } from "./todos.service";

@Module({
  imports: [TypeOrmModule.forFeature([Todo]), UsersModule], // ★
  controllers: [TodosController],
  providers: [TodosService],
})
export class TodosModule {}
src/todos/todos.service.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { UsersService } from "src/users/users.service";
import { Repository } from "typeorm";
import { CreateTodoDto } from "./dto/create-todo.dto";
import { UpdateTodoDto } from "./dto/update-todo.dto";
import { Todo } from "./entities/todo.entity";
import { TodoNotFoundError } from "./errors/todo-not-found-error";

@Injectable()
export class TodosService {
  constructor(
    @InjectRepository(Todo)
    private readonly todosRepository: Repository<Todo>,
    private readonly usersService: UsersService
  ) {}
  // 略
}

アップグレード

Angular でいうng updateみたいなものは無い(あったが削除されたらしい)。

以下でアップグレードはできるがベストかは不明。

1
$ ncu -u "/nestjs*/"