feat: update boilerplate

add swagger
add correlation id
add health check for internet connectivity and database
update to latest nestjs version
remove clutter from http log
This commit is contained in:
Hasta Ragil Saputra 2022-06-24 17:13:05 +07:00
parent e6f8c54d49
commit 0456cd57f4
10 changed files with 1486 additions and 1540 deletions

View File

@ -79,7 +79,7 @@ module.exports = {
'consistent' 'consistent'
], ],
'indent': [ 'indent': [
'warn', 'off',
2 2
], ],
'linebreak-style': 'warn', 'linebreak-style': 'warn',

View File

@ -1,5 +1,5 @@
{ {
"name": "sppbe-gas-meter", "name": "nestjs-boilerplate",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"author": "", "author": "",
@ -21,46 +21,50 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^8.4.0", "@nestjs/axios": "^0.0.8",
"@nestjs/config": "^1.2.0", "@nestjs/common": "^8.4.7",
"@nestjs/core": "^8.4.0", "@nestjs/config": "^2.1.0",
"@nestjs/core": "^8.4.7",
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^8.4.0", "@nestjs/platform-express": "^8.4.7",
"@nestjs/platform-fastify": "^8.4.0", "@nestjs/platform-fastify": "^8.4.7",
"@nestjs/typeorm": "^8.0.3", "@nestjs/swagger": "^5.2.1",
"@nestjs/terminus": "^8.0.8",
"@nestjs/typeorm": "^8.1.4",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
"joi": "^17.6.0", "joi": "^17.6.0",
"nestjs-pino": "^2.5.0", "nestjs-pino": "^2.6.0",
"pg": "^8.7.3", "pg": "^8.7.3",
"pino-http": "^6.6.0", "pino-http": "^8.0.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"typeorm": "^0.2.45", "swagger-ui-express": "^4.4.0",
"typeorm-naming-strategies": "^3.0.0" "typeorm": "^0.3.6",
"typeorm-naming-strategies": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^8.2.2", "@nestjs/cli": "^8.2.8",
"@nestjs/schematics": "^8.0.8", "@nestjs/schematics": "^8.0.11",
"@nestjs/testing": "^8.4.0", "@nestjs/testing": "^8.4.7",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "^27.4.1", "@types/jest": "^28.1.3",
"@types/node": "^17.0.21", "@types/node": "^18.0.0",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.14.0", "@typescript-eslint/parser": "^5.29.0",
"eslint": "^8.10.0", "eslint": "^8.18.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"jest": "^27.5.1", "jest": "^28.1.1",
"prettier": "^2.5.1", "prettier": "^2.7.1",
"supertest": "^6.2.2", "supertest": "^6.2.3",
"ts-jest": "^27.1.3", "ts-jest": "^28.0.5",
"ts-loader": "^9.2.8", "ts-loader": "^9.3.1",
"ts-node": "^10.7.0", "ts-node": "^10.8.1",
"tsconfig-paths": "^3.13.0", "tsconfig-paths": "^4.0.0",
"typescript": "^4.6.2" "typescript": "^4.7.4"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [

View File

@ -5,11 +5,40 @@ import * as Joi from 'joi';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { LoggerModule } from 'nestjs-pino'; import { LoggerModule } from 'nestjs-pino';
import { HealthModule } from './health/health.module';
import configuration from './config/configuration'; import configuration from './config/configuration';
@Module({ @Module({
imports: [ imports: [
LoggerModule.forRoot(), LoggerModule.forRoot({
pinoHttp: {
genReqId: (req) => {
return req['x-correlation-id'];
},
redact: {
paths: [
'req.headers.authorization',
'req.headers["user-agent"]',
'req.headers.accept',
'req.headers["accept-encoding"]',
'req.headers["accept-language"]',
'req.headers.host',
'req.headers.connection',
'req.headers.cookie',
'req.headers["sec-ch-ua"]',
'req.headers["sec-ch-ua-mobile"]',
'req.headers["sec-ch-ua-platform"]',
'req.headers["upgrade-insecure-requests"]',
'req.headers["sec-fetch-site"]',
'req.headers["sec-fetch-mode"]',
'req.headers["sec-fetch-user"]',
'req.headers["sec-fetch-dest"]',
'req.headers["if-none-match"]',
],
remove: true,
},
},
}),
ConfigModule.forRoot({ ConfigModule.forRoot({
load: [configuration], load: [configuration],
validationSchema: Joi.object({ validationSchema: Joi.object({
@ -38,13 +67,14 @@ import configuration from './config/configuration';
entities: [], entities: [],
synchronize: true, synchronize: true,
autoLoadEntities: true, autoLoadEntities: true,
logging: true, logging: false,
namingStrategy: new SnakeNamingStrategy(), namingStrategy: new SnakeNamingStrategy(),
}; };
}, },
inject: [ConfigService], inject: [ConfigService],
}), }),
UsersModule, UsersModule,
HealthModule,
], ],
}) })
export class AppModule {} export class AppModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthController } from './health.controller';
describe('HealthController', () => {
let controller: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
}).compile();
controller = module.get<HealthController>(HealthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,32 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
HttpHealthIndicator,
TypeOrmHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private db: TypeOrmHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => {
return this.http.pingCheck(
'internet-connectivity',
'https://www.google.com',
);
},
() => {
return this.db.pingCheck('database');
},
]);
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [TerminusModule, HttpModule],
controllers: [HealthController],
})
export class HealthModule {}

View File

@ -1,22 +1,19 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Logger } from 'nestjs-pino'; import { Logger } from 'nestjs-pino';
import { CorrelationIdMiddleware } from './utils/correlation-id.middleware';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestExpressApplication>(AppModule);
AppModule,
new FastifyAdapter(),
{ bufferLogs: true },
);
const logger = app.get(Logger); const logger = app.get(Logger);
app.disable('x-powered-by');
app.use(CorrelationIdMiddleware());
app.useLogger(logger); app.useLogger(logger);
app.enableCors(); app.enableCors();
app.useGlobalPipes( app.useGlobalPipes(
@ -25,16 +22,29 @@ async function bootstrap() {
}), }),
); );
const config = new DocumentBuilder()
.setTitle('API Docs')
.setDescription('API description')
.setVersion('1.0')
.addTag('apidocs')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
const configService = app.get<ConfigService>(ConfigService); const configService = app.get<ConfigService>(ConfigService);
const port = configService.get<number>('port'); const port = configService.get<number>('port');
await app.listen(port, '0.0.0.0', (error, address) => { const hostname = '0.0.0.0';
if (error) {
logger.error(error); await app.listen(port, hostname, () => {
process.exit(1); logger.log(`Server listening on ${hostname}:${port}`);
} else { // if (error) {
logger.log(`Server listening on ${address}`); // logger.error(error);
} // process.exit(1);
// } else {
// logger.log(`Server listening on ${address}`);
// }
}); });
} }

View File

@ -15,7 +15,11 @@ export class UsersService {
async create(createUserDto: CreateUserDto) { async create(createUserDto: CreateUserDto) {
const result = await this.usersRepository.insert(createUserDto); const result = await this.usersRepository.insert(createUserDto);
return this.usersRepository.findOneOrFail(result.identifiers[0].id); return this.usersRepository.findOneOrFail({
where: {
id: result.identifiers[0].id,
},
});
} }
findAll() { findAll() {
@ -24,7 +28,11 @@ export class UsersService {
async findOne(id: string) { async findOne(id: string) {
try { try {
return await this.usersRepository.findOneOrFail(id); return await this.usersRepository.findOneOrFail({
where: {
id,
},
});
} catch (e) { } catch (e) {
if (e instanceof EntityNotFoundError) { if (e instanceof EntityNotFoundError) {
throw new HttpException( throw new HttpException(
@ -42,7 +50,11 @@ export class UsersService {
async update(id: string, updateUserDto: UpdateUserDto) { async update(id: string, updateUserDto: UpdateUserDto) {
try { try {
await this.usersRepository.findOneOrFail(id); await this.usersRepository.findOneOrFail({
where: {
id,
},
});
} catch (e) { } catch (e) {
if (e instanceof EntityNotFoundError) { if (e instanceof EntityNotFoundError) {
throw new HttpException( throw new HttpException(
@ -57,14 +69,22 @@ export class UsersService {
} }
} }
const result = await this.usersRepository.update(id, updateUserDto); await this.usersRepository.update(id, updateUserDto);
return this.usersRepository.findOneOrFail(id); return this.usersRepository.findOneOrFail({
where: {
id,
},
});
} }
async remove(id: string) { async remove(id: string) {
try { try {
await this.usersRepository.findOneOrFail(id); await this.usersRepository.findOneOrFail({
where: {
id,
},
});
} catch (e) { } catch (e) {
if (e instanceof EntityNotFoundError) { if (e instanceof EntityNotFoundError) {
throw new HttpException( throw new HttpException(

View File

@ -0,0 +1,12 @@
import * as uuid from 'uuid';
export function CorrelationIdMiddleware() {
return (req, res, next: () => void) => {
const correlationHeader = req.headers['x-correlation-id'] || uuid.v4();
// eslint-disable-next-line no-param-reassign
req.headers['x-correlation-id'] = correlationHeader;
res.header('X-Correlation-Id', correlationHeader);
next();
};
}

2784
yarn.lock

File diff suppressed because it is too large Load Diff