0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NestJS を使う

Posted at

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

インストール

公式の方法は以下。

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

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

$ 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を作ってくれる。

$ 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
export class Todo {
  id: string;
  name: string;
  done: boolean;
}
src/todos/todos.service.ts
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
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
import { IsBoolean, IsNotEmpty, IsString } from "class-validator";

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

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

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

$ npm i class-validator class-transformer

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

src/todos/dto/create-todo.dto.ts
import { IsNotEmpty } from "class-validator";

export class CreateTodoDto {
  @IsNotEmpty()
  name: string;
  @IsNotEmpty()
  done: boolean;
}
ts title="src/main.ts"
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();

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

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

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

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

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

src/todos/errors/todo-not-found-error.ts
export class TodoNotFoundError extends Error {}
src/todos/todos.service.ts
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
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

インターセプター

CORS

app.enableCors()を使う。

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
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
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
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みたいなものは無い(あったが削除されたらしい)。

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

$ ncu -u "/nestjs*/"
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?