개요 : spring on express
npx - 설치없이 사용
npm i -g @nestjs/cli
nest new backend
dependency managing: npm
- controller
- module - controllers, providers 등록
- 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 {}
middleware는 appModule에 가급적 등록
swagger
- 문서화
npm install --save @nestjs/swagger swagger-ui-express
hot reload
→ 전체 reload가 아닌 특정 부분만 reload 되게 해준다.
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],
})
$ nest g co todo
$ nest g s todo
$ nest g mo todo
module이 대가리
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;
}
- Controller → 요청 받는 곳
- Repository
- Entity
- Service → 요청에 대한 응답하는 곳
비동기(async) → 프론트랑 백이랑 통신할때, DB랑 백이랑 통신할때 등...
- callback, promise, async-await
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 {}
$ 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;
}
service를 만들면 controller에 붙인다!
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();
}
}
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);
}
}
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.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);
}
}