nestjsでtodolist-1を作成します.backend API


概要:springon express
npx-インストールなしで使用
npm i -g @nestjs/cli
nest new backend

dependency managing: npm
  • controller
  • モジュール-コントローラ、仕入先登録
  • service
  • repository
  • entity
  • module → controller → service
    decorator(nestJS) === annotation(java)
    @Injectable()=Dependency Injection:結合緩和
    nest g(enarate) mo(dule) users
    nest g(enarate) co(ntroller) users
    nest g(enarate) s(ervice) users
    
    nest g(enarate) res(ource) posts
    -> REST API
    middleware : request → middleware → response
    export class LoggerMiddleware implements NestMiddleware {}
    ミドルウェアはappModuleに登録できます
    swagger
  • ドキュメント化
  • npm install --save @nestjs/swagger swagger-ui-express
    hot reload
    →すべてを再ロードするのではなく、特定の部分だけを再ロードします.
    postgres & postico
    postgresqlのインストール
    $ brew install postgresql
    実行
    $ brew services start postgresql
    sequelize(express) typeorm(typescript)/prisma(JPA)
    $ npm install --save pg @nestjs/typeorm
    psql接続
    psql postgres
    typeorm登録→app.module.tsに登録する
    @Module({
      imports: [
        TypeOrmModule.forRoot({
          type: 'postgres',
          host: 'localhost',
          port: 5432,
          username: 'eugene',
          password: '920512',
          database: 'todolist-nest-react',
          entities: [],
          synchronize: true,
          logging: true,
          keepConnectionAlive: true,
        }),
      ],
      //controllers: [AppController],
      //providers: [AppService],
    })
    .env-サーバ環境変数
    @Module({
      imports: [
        ConfigModule.forRoot(),
        TypeOrmModule.forRoot({
          type: 'postgres',
          host: process.env.DB_HOST,
          port: +process.env.DB_PORT,
          username: process.env.DB_USERNAME,
          password: process.env.DB_PASSWORD,
          database: process.env.DB_DATABASE,
          entities: [],
          synchronize: true,
          logging: true,
          keepConnectionAlive: true,
        }),
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    todolist
    $ nest g co todo
    $ nest g s todo
    $ nest g mo todo

    entity
    todo → entities → todo.entity.ts
    import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
    
    @Entity()
    export class Todo {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column('varchar')
      title: string;
    
      @Column('varchar')
      description: string;
    
      @Column('boolean', { default: false })
      isDone: boolean;
    
      @CreateDateColumn()
      createdAt: Date;
    
      @UpdateDateColumn()
      updatedAt: Date;
    }
  • コントローラ→
  • リクエスト
  • Repository
  • Entity
  • サービス→リクエストに応答する
  • 非同期(async)→フロントやバックグラウンドとの通信時、DBやバックグラウンドとの通信時など…
  • callback, promise, async-await
  • C(Create)
    todo.service.ts
    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { Todo } from './entities/todo.entity';
    
    @Injectable()
    export class TodoService {
      constructor(
        @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
      ) {}
    
      async createTodo(CreateTodoDto: CreateTodoDto) {
        return await this.todoRepository.save(CreateTodoDto);
      }
    }
    todo.controller.ts
    import { Body, Controller, Post } from '@nestjs/common';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { TodoService } from './todo.service';
    
    @Controller('todo')
    export class TodoController {
      constructor(private readonly todoService: TodoService) {}
    
      @Post()
      async createTodo(@Body() createTodoDto: CreateTodoDto) {
        return await this.todoService.createTodo(createTodoDto);
      }
    }
    todo.module.ts
    import { Module } from '@nestjs/common';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Todo } from './entities/todo.entity';
    import { TodoController } from './todo.controller';
    import { TodoService } from './todo.service';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Todo])], // DB
      controllers: [TodoController], 
      providers: [TodoService],// Service
    })
    export class TodoModule {}
    R(Read)
    $ npm i class-validator class-transformer
    main.ts
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      // read
      app.useGlobalPipes(new ValidationPipe());
    
      const config = new DocumentBuilder()
        .setTitle('Todolist API')
        .setDescription('This is todolist API.')
        .setVersion('1.0')
        .addCookieAuth('connect.sid')
        .build();
    dtos/createTodo.dto.ts
    import { IsString } from 'class-validator';
    
    export class CreateTodoDto {
      @IsString()
      title: string;
    
      @IsString()
      desc: string;
    }
    PickType(nestjs/swagger)
    ↔ omit
    import { PickType } from '@nestjs/swagger';
    import { Todo } from '../entities/todo.entity';
    
    export class CreateTodoDto extends PickType(Todo, ['title', 'desc'] as const) {}
    todo.entity.ts
    import { ApiProperty } from '@nestjs/swagger';
    import { IsString } from 'class-validator';
    import {
      Column,
      CreateDateColumn,
      Entity,
      PrimaryGeneratedColumn,
      UpdateDateColumn,
    } from 'typeorm';
    
    @Entity()
    export class Todo {
      @PrimaryGeneratedColumn()
      id: number;
    
      @IsString()
      @ApiProperty({
        example: 'Eat',
        description: '투두리스트 제목',
      })
      @Column('varchar')
      title: string;
    
      @IsString()
      @ApiProperty({
        example: 'get energy',
        description: '투두리스트 설명',
      })
      @Column('varchar')
      desc: string;
    
      @Column('boolean', { default: false })
      isDone: boolean;
    
      @CreateDateColumn()
      createdAt: Date;
    
      @UpdateDateColumn()
      updatedAt: Date;
    }
    サービスを作成したら、コントローラに貼り付けます!
    response logging(with swagger)
    todo.controller.ts
    import { Body, Controller, Get, Post } from '@nestjs/common';
    import { ApiResponse, ApiTags } from '@nestjs/swagger';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { Todo } from './entities/todo.entity';
    import { TodoService } from './todo.service';
    
    @ApiTags('todo')
    @Controller('todo')
    export class TodoController {
      constructor(private readonly todoService: TodoService) {}
    
    	// swagger logging
      @ApiResponse({
        status: 201,
        description: 'creating new todo',
        type: Todo,
      })
      @Post()
      async createTodo(@Body() createTodoDto: CreateTodoDto) {
        return await this.todoService.createTodo(createTodoDto);
      }
    
    	// swagger logging
      @ApiResponse({
        status: 200,
        description: 'get todolist',
        type: [Todo],
      })
      @Get()
      async getTodos() {
        return await this.todoService.getTodos();
      }
    }
    U(Update)
    todo.service.ts
    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { Todo } from './entities/todo.entity';
    
    @Injectable()
    export class TodoService {
      constructor(
        @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
      ) {}
    
      async createTodo(createTodoDto: CreateTodoDto) {
        return await this.todoRepository.save(createTodoDto);
      }
    
      async getTodos() {
        return await this.todoRepository.find();
      }
    
      async updateTodo(param, { title, desc }) {
        const todo = await this.todoRepository.findOne({
          where: {
            id: param.todoId,
          },
        });
    
        todo.title = title;
        todo.desc = desc;
    
        return this.todoRepository.save(todo);
      }
    }
    updateTodo.dto.ts
    import { PickType } from '@nestjs/swagger';
    import { IsOptional } from 'class-validator';
    import { Todo } from '../entities/todo.entity';
    
    export class UpdateTodoDto extends PickType(Todo, ['title', 'desc'] as const) {
      @IsOptional()
      title: string;
    
      @IsOptional()
      desc: string;
    }
    todo.service.ts
    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    
    @Injectable()
    export class TodoService {
      constructor(
        @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
      ) {}
    
      async createTodo(createTodoDto: CreateTodoDto) {
        return await this.todoRepository.save(createTodoDto);
      }
    
      async getTodos() {
        return await this.todoRepository.find();
      }
    
      async updateTodo(param, updateTodoDto: UpdateTodoDto) {
        const todo = await this.todoRepository.findOne({
          where: {
            id: param.todoId,
          },
        });
    
        todo.title = updateTodoDto.title;
        todo.desc = updateTodoDto.desc;
    
        return this.todoRepository.save(todo);
      }
    }
    todo.controller.ts
    import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
    import { ApiResponse, ApiTags } from '@nestjs/swagger';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    import { TodoService } from './todo.service';
    
    @ApiTags('todo')
    @Controller('todo')
    export class TodoController {
      constructor(private readonly todoService: TodoService) {}
    
      @ApiResponse({
        status: 201,
        description: 'creating new todo',
        type: Todo,
      })
      @Post()
      async createTodo(@Body() createTodoDto: CreateTodoDto) {
        return await this.todoService.createTodo(createTodoDto);
      }
    
      @ApiResponse({
        status: 200,
        description: 'get todolist',
        type: [Todo],
      })
      @Get()
      async getTodos() {
        return await this.todoService.getTodos();
      }
    
      @Put('/:todoId')
      async updateTodo(
        @Param() param: { todoId: string },
        @Body() updateTodoDto: UpdateTodoDto,
      ) {
        return await this.todoService.updateTodo(param, updateTodoDto);
      }
    }
    値が含まれていない場合の処理
    todo.service.ts
    import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    
    @Injectable()
    export class TodoService {
      constructor(
        @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
      ) {}
    
      async createTodo(createTodoDto: CreateTodoDto) {
        return await this.todoRepository.save(createTodoDto);
      }
    
      async getTodos() {
        return await this.todoRepository.find();
      }
    
      async updateTodo(param, updateTodoDto: UpdateTodoDto) {
        const todo = await this.todoRepository.findOne({
          where: {
            id: param.todoId,
          },
        });
    
        if (!updateTodoDto.title && !updateTodoDto.desc) {
          //400 error
          throw new HttpException(
            '최소 하나의 값이 필요합니다',
            HttpStatus.FORBIDDEN,
          );
        }
    
        todo.title = updateTodoDto.title;
        todo.desc = updateTodoDto.desc;
    
        return this.todoRepository.save(todo);
      }
    }
    D(Delete)
    todo.service.ts
    import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    
    @Injectable()
    export class TodoService {
      constructor(
        @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
      ) {}
    
      async createTodo(createTodoDto: CreateTodoDto) {
        return await this.todoRepository.save(createTodoDto);
      }
    
      async getTodos() {
        return await this.todoRepository.find();
      }
    
      async updateTodo(param, updateTodoDto: UpdateTodoDto) {
        const todo = await this.todoRepository.findOne({
          where: {
            id: param.todoId,
          },
        });
    
        if (!updateTodoDto.title && !updateTodoDto.desc) {
          //400 error
          throw new HttpException(
            '최소 하나의 값이 필요합니다',
            HttpStatus.FORBIDDEN,
          );
        }
    
        todo.title = updateTodoDto.title;
        todo.desc = updateTodoDto.desc;
    
        return this.todoRepository.save(todo);
      }
    
      async deleteTodo(param: { todoId: string }) {
        return await this.todoRepository.delete(param.todoId);
      }
    }
    todo.controller.ts
    import {
      Body,
      Controller,
      Delete,
      Get,
      Param,
      Post,
      Put,
    } from '@nestjs/common';
    import { ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    import { TodoService } from './todo.service';
    
    @ApiTags('todo')
    @Controller('todo')
    export class TodoController {
      constructor(private readonly todoService: TodoService) {}
    
      @ApiResponse({
        status: 201,
        description: 'creating new todo',
        type: Todo,
      })
      @Post()
      async createTodo(@Body() createTodoDto: CreateTodoDto) {
        return await this.todoService.createTodo(createTodoDto);
      }
    
      @ApiResponse({
        status: 200,
        description: 'get todolist',
        type: [Todo],
      })
      @Get()
      async getTodos() {
        return await this.todoService.getTodos();
      }
    
      @ApiParam({
        name: 'todoId',
        required: true,
        description: 'todo id',
      })
      @Put(':todoId')
      async updateTodo(
        @Param() param: { todoId: string },
        @Body() updateTodoDto: UpdateTodoDto,
      ) {
        return await this.todoService.updateTodo(param, updateTodoDto);
      }
    
      @ApiParam({
        name: 'todoId',
        required: true,
        description: 'todo id',
      })
      @Delete(':todoId')
      async deleteTodo(@Param() param: { todoId: string }) {
        return await this.todoService.deleteTodo(param);
      }
    }
    todo時のサービス
  • todoをクリックすると、isComplete値を反対の
  • に変更します.
    todo.service.ts
    import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    
    @Injectable()
    export class TodoService {
      constructor(
        @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
      ) {}
    
      async createTodo(createTodoDto: CreateTodoDto) {
        return await this.todoRepository.save(createTodoDto);
      }
    
      async getTodos() {
        return await this.todoRepository.find();
      }
    
      async updateTodo(param, updateTodoDto: UpdateTodoDto) {
        const todo = await this.todoRepository.findOne({
          where: {
            id: param.todoId,
          },
        });
    
        if (!updateTodoDto.title && !updateTodoDto.desc) {
          //400 error
          throw new HttpException(
            '최소 하나의 값이 필요합니다',
            HttpStatus.FORBIDDEN,
          );
        }
    
        todo.title = updateTodoDto.title;
        todo.desc = updateTodoDto.desc;
    
        return this.todoRepository.save(todo);
      }
    
      async deleteTodo(param: { todoId: string }) {
        return await this.todoRepository.delete(param.todoId);
      }
    
      async toggleComplete(param: { todoId: string }) {
        const todo = await this.todoRepository.findOne({
          where: {
            id: +param.todoId,
          },
        });
    
        todo.isComplete = !todo.isComplete;
    
        return await this.todoRepository.save(todo);
      }
    }
    todo.controller.ts
    import {
      Body,
      Controller,
      Delete,
      Get,
      Param,
      Post,
      Put,
    } from '@nestjs/common';
    import { ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
    import { CreateTodoDto } from './dtos/createTodo.dto';
    import { UpdateTodoDto } from './dtos/updateTodo.dto';
    import { Todo } from './entities/todo.entity';
    import { TodoService } from './todo.service';
    
    @ApiTags('todo')
    @Controller('todo')
    export class TodoController {
      constructor(private readonly todoService: TodoService) {}
    
      @ApiResponse({
        status: 201,
        description: 'creating new todo',
        type: Todo,
      })
      @Post()
      async createTodo(@Body() createTodoDto: CreateTodoDto) {
        return await this.todoService.createTodo(createTodoDto);
      }
    
      @ApiResponse({
        status: 200,
        description: 'get todolist',
        type: [Todo],
      })
      @Get()
      async getTodos() {
        return await this.todoService.getTodos();
      }
    
      @ApiParam({
        name: 'todoId',
        required: true,
        description: 'todo id',
      })
      @Put(':todoId')
      async updateTodo(
        @Param() param: { todoId: string },
        @Body() updateTodoDto: UpdateTodoDto,
      ) {
        return await this.todoService.updateTodo(param, updateTodoDto);
      }
    
      @ApiParam({
        name: 'todoId',
        required: true,
        description: 'todo id',
      })
      @Delete(':todoId')
      async deleteTodo(@Param() param: { todoId: string }) {
        return await this.todoService.deleteTodo(param);
      }
    
      @ApiParam({
        name: 'todoId',
        required: true,
        description: 'todo id',
      })
      @ApiResponse({
        status: 200,
        description: 'todo success',
        type: Todo,
      })
      @Put('complete/:todoId')
      async toggleComplete(@Param() param: { todoId: string }) {
        return await this.todoService.toggleComplete(param);
      }
    }