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 resource
でController
、Service
、Module
を作ってくれる。
$ 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.
export class Todo {
id: string;
name: string;
done: boolean;
}
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);
}
}
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);
}
}
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 にデコレータをつける。
import { IsNotEmpty } from "class-validator";
export class CreateTodoDto {
@IsNotEmpty()
name: string;
@IsNotEmpty()
done: boolean;
}
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 することにする。
export class TodoNotFoundError extends Error {}
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;
}
}
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 を参照する例を示す。
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 {}
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 {}
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*/"