nestjs cli ๋ฅผ ์ด์ฉํ๋ฉด ์ฝ๊ฒ ํ๋ก์ ํธ๋ฅผ ์์ฑ ํ ์ ์์ต๋๋ค.
# nestjs cli ์ค์น
$ npm i -g @nestjs/cli
# ํ๋ก์ ํธ ์์ฑ
$ nest new project-name
2. OpenAPI ์ค์
๋ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํฉ๋๋ค.
$ npm install --save @nestjs/swagger swagger-ui-express
์ดํ
main.ts
์ SwaggerModule ์ค์ ์ ํฉ๋๋ค
# main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from "@nestjs/platform-express";
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const options = new DocumentBuilder()
.setTitle('ํ๋ก์ ํธ ๋ช
')
.setDescription('ํ๋ก์ ํธ ์ค๋ช
')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document);
await app.listen(process.env.PORT);
}
bootstrap();
3. Validation
request ์์ฒญ์ ๋ํ Validation(๊ฒ์ฆ) ์ฒ๋ฆฌ๋ฅผ ์ํด ๋ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ค์น๊ฐ ํ์ ํฉ๋๋ค.
$ npm install class-validator
Global ์ค์ ์ main.ts์ ์ถ๊ฐ ํฉ๋๋ค. Transform payload objects ์ค์ ์ ํ์ฌ ์๋ ๋ณํ ์ฒ๋ฆฌ๋ฅผ ํฉ๋๋ค.
# main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from "@nestjs/platform-express";
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useGlobalPipes(new ValidationPipe({transform: true})); // Validate with ์๋ ๋ณํ ์ฒ๋ฆฌ
const options = new DocumentBuilder()
.setTitle('ํ๋ก์ ํธ ๋ช
')
.setDescription('ํ๋ก์ ํธ ์ค๋ช
')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document);
await app.listen(process.env.PORT);
}
bootstrap();
์ดํ ์์ ๋ auto-validation์์ ํ์ธํ์ธ์.
express์์๋ dotenv๋ฅผ ์ฌ์ฉํ์๋๋ฐ, nestjs์์๋ Configuration์ด ์ ๊ณต ๋ฉ๋๋ค. ์ค์น๋ ์๋์ ๊ฐ์ต๋๋ค.
$ npm i --save @nestjs/config
์ฌ์ฉ๋ฒ์
app.module.ts
์ ์ ์ธ์ ํ์ฌ์ ์ฌ์ฉํฉ๋๋ค. isGlobal ์ค์ ์ ํด๋๋ฉด ๋ค๋ฅธ ๋ชจ๋์์ imports ํ์ง ์๊ณ ์ฌ์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก .env ํ์ผ์ ์ฝ์ด์ ๋ณ์ํ ํ์ฌ ์ฌ์ฉํฉ๋๋ค.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
๋ ์์ธํ ์์ ๋ Using the ConfigService๋ฅผ ์ฐธ์กฐํ์ธ์.
4. Logger
nestjs์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก Logger class๋ฅผ ์ ๊ณตํฉ๋๋ค.
main.js
์์ ApplicationModule์์ logger ์ต์ ์ ์ด์ฉํด์ ๋ ๋ฒจ์ ์ค์ ํ ์ ์์ต๋๋ค. ๋ ๋ฒจ์ 'log', 'error', 'warn', 'debug', 'verbose' ์ด ์์ต๋๋ค.
const app = await NestFactory.create(ApplicationModule, {
logger: ['error', 'warn'],
});
await app.listen(3000);
์ฌ์ฉ๋ฒ์ Using the logger for application logging ๊ฐ์ด ์ฃผ์ ํ์ฌ ์ฌ์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
import { Logger, Injectable } from '@nestjs/common';
@Injectable()
class MyService {
private readonly logger = new Logger(MyService.name);
doSomething() {
this.logger.log('Doing something...');
}
}
5. session
์ธ์ ์ express๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ express-session ๋ชจ๋์ ์ค์นํด์ ์ฌ์ฉํฉ๋๋ค.
$ npm i express-session
์ฌ์ฉ๋ฒ์
main.ts
์ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํฉ๋๋ค.(์ด๋ express-session ์ฌ์ฉ๋ฒ์ ์์ธํ ๋ณด์ธ์.)
import * as session from 'express-session';
// somewhere in your initialization file
app.use(
session({
secret: 'my-secret',
resave: false,
saveUninitialized: false,
}),
);
helmet ๊ณผ cors ์ค์ ์ ๋ค๋ฅธ ์ค์ ํจ์ ๋ณด๋ค ๋จผ์ ์ค์ ๋์ด์ผ ํฉ๋๋ค. ๋ง์ฝ ๊ฒฝ๋ก๋ฅผ ์ ์ํ ํ helmet๊ณผ cors๋ฅผ ์ค์ ํ ๊ฒฝ์ฐ ์ด๋ฏธ ์ค์ ๋ ๊ฒฝ๋ก์ ๋ฏธ๋ค์จ์ด๋ ์ ์ฉ๋์ง ์์ ์ ์์ต๋๋ค.
helmet์ http ํด๋๋ฅผ ์ ์ ํ๊ฒ ์ค์ ํ์ฌ ์น ์ทจ์ฝ์ ์ผ๋ก ๋ถํฐ ์ฑ์ ๋ณดํธ ํฉ๋๋ค. ์ค์น
$ npm i --save helmet
์ฌ์ฉ๋ฒ
import * as helmet from 'helmet';
// somewhere in your initialization file
app.use(helmet());
cors๋ ๋ค๋ฅธ ๋๋ฉ์ธ์์ ๋ฆฌ์์ค๋ฅผ ์์ฒญ ํ ์ ์๋๋กํ๋ ์ค์ ์ ๋๋ค. ์ค์น๋ ๋ฐ๋ก ํ์ ์์ต๋๋ค. ์ฌ์ฉ๋ฒ์ ์๋์ ๊ฐ์ต๋๋ค.
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
๋ค์์ ์ ๊ฐ ์ฌ์ฉํ ์์ ์ ๋๋ค.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from "@nestjs/platform-express";
import * as helmet from 'helmet';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import * as session from 'express-session';
import * as passport from 'passport';
import flash = require('connect-flash');
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.setGlobalPrefix('api'); // prefix ์ค์
app.useGlobalPipes(new ValidationPipe({ transform: true })); // validate ์ฌ์ฉ ์ค์
app.use(helmet({
contentSecurityPolicy: false,
})); // helmet ์ค์ ๊ณผ CSP ์ ์ธ (google analytics ์ฌ์ฉ์ ์ ์ธ ํด์ผํจ)
app.enableCors({
origin: [
/^(.*)/,
],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
preflightContinue: false,
credentials: true,
optionsSuccessStatus: 204,
allowedHeaders:
'Origin,X-Requested-With,Content-Type,Accept,Authorization,authorization,X-Forwarded-for',
}); // cors ์ค์ credentials ์ค์ ์ ํด์ผ credentials ์ ๋ณด๋ ํจ๊ป ์ ๋ฌํจ
const options = new DocumentBuilder()
.setTitle('ํ๋ก์ ํธ ๋ช
')
.setDescription('ํ๋ก์ ํธ ์ค๋ช
')
.setVersion('1.0')
.addBearerAuth() // openapi ๋ฌธ์์์ ๊ถํ ์ฒ๋ฆฌ ์ถ๊ฐ
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document); // openapi ์ฌ์ฉ
app.use(session({
secret: process.env.SECCRET || 'keyboard cat',
resave: false,
saveUninitialized: false,
})); // session ์ฌ์ฉ
// passport ์ค์
app.use(passport.initialize());
app.use(passport.session());
// request์ ๊ฐ์ ์ถ๊ฐ ํ๋ flash ์ถ๊ฐ
app.use(flash());
await app.listen(process.env.PORT);
}
bootstrap();
- nestjs 10.3.0
- ์ ๋ฐ์ ์ธ ๋ชจ๋ ์ ๋
- npm => pnpm ๋ชจ๋์ ์ด์ฉํ ์ค์น๋ก ๋ณ๊ฒฝ
src/common/firebase
์ดํ firebase ์ฐ๊ณ ์ฒ๋ฆฌ ์ถ๊ฐ
- firebase ์์ ํ๋ก์ ํธ ์์ฑ ํ ์งํ
firebase.config.json
ํ์ผ์ดsrc/common/firebase
๋๋ ํ ๋ฆฌ์ ์กด์ฌ ํด์ผ ํจhttp://localhost:3000/firebaseInfo
API ํธ์ถ์header
์authorization
์ถ๊ฐํด์ ํธ์ถ์firebase
์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํ์ธ
์ฃผ์ ๋ณ๊ฒฝ ๋ด์ญ์ ์๋์ ๊ฐ์ต๋๋ค.
- nestjs 9.3.x ์ ์ฉ
- typeorm 0.3.x ์ ์ฉ
nestjs-pino ์ ์ฉ (2023.01.17)
๋ก๊น ์ ์์ฒญ ์ ๋ณด๋ฅผ ํจ๊ป ํ์ํ์ฌ, ๊ฐ ๋ก๊ทธ๋ณ ์ฐ๊ฒฐ ์ฒ๋ฆฌ๋ฅผ ํ์์ต๋๋ค. ์์ธ๋ด์ฉ
์ถ๊ฐ ์ค์น ๋ชจ๋ ์ ๋ณด
throttler-ratelimiting ์ฒ๋ฆฌ (2022.09.16)
sequenceDiagram;
autonumber;
actor User;
loop 60์ด ๋์ 4๊ฐ ์ ํ
User->>+ valid: ๊ณ์ ํ์ธ;
valid -->>- User: ์ฑ๊ณต;
end;
- @nestjs/throttler
throttler ๋ชจ๋์ ๋ณ๊ฒฝํด์ ๋ก๊ทธ์ธ ๊ณ์ ๊ธฐ์ค์ผ๋ก ํน์ ์๊ฐ๋์ ์์ฒญ ์ ํ ์ฒ๋ฆฌ
- ์ถ๊ฐ ๋ฐ ๋ณ๊ฒฝ ์์ค ๋ชฉ๋ก
- src/app.module.ts : throttler ๋ชจ๋ ์ค์
- src/app.controller.ts : ์ฌ์ฉ ์์ (valid method)
- src/common/core/throttler.guard.ts : guard ์ค์
- src/common/core/throttler-storage-redis.service.ts_b : redis๋ฅผ storage ์ฌ์ฉ์ ์์
chaching ์ฒ๋ฆฌ (2022.09.11)
graph LR;
request[request] -->|URI| get{GET};
get --> |Y| nocache{NoCache};
get --> |N| callP(next);
nocache --> |Y| callng(next);
nocache --> |N| hascache{HasCache};
hascache --> |N| callg(next);
callg --> savecache(SaveCache);
hascache --> |Y| getcache(GetCache);
callP --> hasevict{hasEvict};
hasevict --> |Y| keys(getKeys);
hasevict --> |N| uri(uri);
keys --> removecache(removeCache);
uri --> removecache;
removecache --> return[RETURN];
callng --> return;
getcache --> return;
savecache --> return;
- httpcache.interceptor.ts
URL ๊ธฐ์ค์ผ๋ก http method๊ฐ
get
์ผ ๊ฒฝ์ฐ ๋ฐ์ดํฐ ๊ฐ์ ์ ์ฅ ์ฒ๋ฆฌํ๋ฉฐpost
,delete
,put
๋ฑ์ด ํธ์ถ ๋ ๊ฒฝ์ฐ URL ๊ธฐ์ค์ผ๋ก ๊ธฐ์กด ์บ์๋ฅผ ์ญ์ - cache.decorator.ts
@NoCache
์บ์ ์์ธ ์ฒ๋ฆฌ@CacheEvict
์บ์ ์ญ์ ์ ๋ค๋ฅธ url ํจ๊ป ์ฒ๋ฆฌ
src/common/config
์ ๊ฐ ํญ๋ชฉ์ ์ค์ ํ์ผ์ ์ ์ฌํ ์ฌ์ฉ
- logging
- database
- SnakeNamingStrategy ์ถ๊ฐ
- QueryLogger ์ถ๊ฐ
.env
์์ ํ์ํ ์์๋ฅผ ๋ก๋ํ๋ ์ฒ๋ฆฌ
- logger์ ๋ํ ์์ ์ฒ๋ฆฌ
- typeorm์ ๋ํ ์์ ์ฒ๋ฆฌ
src/app.module.ts
ํ์ผ์์ forRootAsync
๊ณผ useFactory
์ ์ด์ฉํ์ฌ .env
ํ์ผ์ ์์๋ฅผ ํ์ฉ
...
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'sqlite',
database: configService.get('DB_HOST'),
dropSchema: configService.get('DB_DROP') === 'true',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: configService.get('DB_SYNC') === 'true',
logging: configService.get('DB_LOGGING') === 'true',
logger: 'file',
}),
}),
....
- winston : winston
- nest-winston : nestjs์์ winstion ์ฌ์ฉ ์ฒ๋ฆฌ
- winston-daily-rotate-file : ๋ ์ง๋ณ ํ์ผ ์์ฑ ๋ฐ ๊ด๋ฆฌ ์ง์
# nestjs์์ winston ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ์ถ๊ฐ
$ npm i nest-winston winston winston-daily-rotate-file
main.js์์ ์๋์ ๊ฐ์ด ์ค์ ํ์ฌ ์ฌ์ฉ
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
...
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new winston.transports.Console(),
new (require('winston-daily-rotate-file'))({
dirname: path.join(__dirname, './../logs/debug/'), // ํ์ผ ์ ์ฅ ์์น
filename: 'debug-%DATE%.log', // ํ์ผ ๋ช
datePattern: 'YYYY-MM-DD-HH', // ํ์ผ๋ช
์ ๋ ์ง(DATE) ํจํด
level: 'debug', // ๋ก๊ทธ ๋ ๋ฒจ
zippedArchive: true, //์์ถ ์ฌ๋ถ
maxSize: '20m', // ํ๊ฐ์ ํ์ผ ์ต๋ ํฌ๊ธฐ
maxFiles: '14d' // ํ์ผ์ ์ต๋ ์ ์ง ๋ ์ง
}),
new (require('winston-daily-rotate-file'))({
dirname: path.join(__dirname, './../logs/info/'),
filename: 'info-%DATE%.log',
datePattern: 'YYYY-MM-DD-HH',
level: 'info',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
}),
],
})
});
...
src/common/middleware/AppLoggerMiddleware.ts
ํ์ผ ์ถ๊ฐ ๋ฐ src/app.module.ts
์ ๋ค์ ๋ด์ฉ ์ถ๊ฐ
// src/common/middleware/AppLoggerMiddleware.ts
import { Request, Response, NextFunction } from "express";
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger = new Logger("HTTP");
use(request: Request, response: Response, next: NextFunction): void {
const { ip, method, originalUrl } = request;
const userAgent = request.get("user-agent") || "";
response.on("finish", () => {
const { statusCode } = response;
const contentLength = response.get("content-length");
this.logger.log(
`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`,
);
if (method !== 'GET') {
this.logger.debug(request.body);
}
});
next();
}
}
// src/app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(AppLoggerMiddleware).forRoutes('*');
}
}
ํ ์คํธ ์ผ์ด์ค๋ฅผ ์ถ๊ฐํ์์ต๋๋ค.
user.controller.spec.ts ํ์ผ์ ๊ธฐ์ค์ผ๋ก End to End ํ ์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํ์์ต๋๋ค.
npm run test
node node_modules/jest/bin/jest.js src/user/user.controller.spec.ts
- typeorm + sqlite ์กฐํฉ์ ์ฌ์ฉ์ CRUD ๊ตฌํ
- passport + local strategy๋ฅผ ์ด์ฉํ ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ์ฒ๋ฆฌ
- test-auth-chapter-sample๋ฅผ ์ฐธ์กฐํ์ฌ ๊ตฌํ ํ์์ต๋๋ค.