Setting up nestjs project

1 - Generating the project
nest new <project name>
Which package manager would you ❤️ to use?
I choose npm
cd <project name>
2 - Setup auto format vs code
I create a file .vscode/settings.json
{
"files.eol": "\n",
"editor.formatOnSave": true,
"editor.tabSize": 2
}
3 - Changing adaptater to fastify
I followed the official documentation.
First I installed the fastify package: npm i --save @nestjs/platform-fastify
Then I updated the main.ts
file to look like that
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000);
}
bootstrap();
Then I removed the express package
npm remove @nestjs/platform-express
4 - Validation pipe
When data are sent to our application we want it to be sure it is valid. The validation pipe will enfore and validate the structure of the data for each payloads.
The description of the data will be done on the DTO (Data Transfer Object) class using decorators.
Here is a simple example of a DTO class
export class CreateUserDto {
@IsString()
readonly name: string;
@IsString()
readonly email: string;
@IsString({ each: true })
readonly roles: string[];
}
We will need to install some packages, theses one will allow us to use the decorators and the validation pipe.
npm i --save class-validator class-transformer
The configuration of the validation pipe is done in the main.ts
file.
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Every property that is not in the DTO will be removed
forbidNonWhitelisted: true, // If a property is not in the DTO an error will be thrown because it is not allowed
transform: true, // Transform the payload into the DTO class, without this option the payload will be a simple object with the shape of the DTO class but won't be an instance of the DTO class.
transformOptions: {
enableImplicitConversion: true, // Convert the type primitive type like number and boolean to the right type without having to specify it with @Type decorator
},
}),
);
await app.listen(3000);
}
To manage some other case we will need another package, this one will allow us to use the other decorators like the PartialType
decorator.
npm i @nestjs/mapped-types
Here is an example of transformation, the PartialType
decorator will allow us to transform a DTO class into a partial class, all the properties are optional for the payload. And it will also avoid redundant code.
export class UpdateUserDto extends PartialType(CreateUserDto) {}
5 - Configure Interceptor
We can apply the ClassSerializerInterceptor
globally to the application, it will allow us to use the class-transformer decorators. For example we can exclude some properties from the response.
In the main.ts file
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
6 - Configure swagger
npm install --save @nestjs/swagger @fastify/static)
in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document, {
swaggerOptions: {
persistAuthorization: true,
defaultModelsExpandDepth: -1,
docExpansion: 'none',
filter: true,
syntaxHighlight: {
activate: true,
theme: 'tomorrow-night',
},
tryItOutEnabled: true,
},
});
await app.listen(3000);
}
Inside the nest-cli.json you need to add the plugin
"compilerOptions": {
"deleteOutDir": true,
"plugins": ["@nestjs/swagger/plugin"] // 👈
}
7 - Configuring Database
First we will prepare the configuration by installing the config package
npm i @nestjs/config
Since we want to add a validation of the environment variables we will install the package joi
npm install joi
We will need to create a .env
file at the root of the project, this file will contain the configuration of the database.
DATABASE_USER=postgres
DATABASE_PASSWORD=pass123
DATABASE_NAME=postgres
DATABASE_PORT=5432
DATABASE_HOST=localhost
Then inside the app module we will import the config module and configure it. Be careful the import of Joi is import * as Joi from 'joi';
and not import Joi from 'joi';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_USER: Joi.required(),
DATABASE_PASSWORD: Joi.required(),
DATABASE_NAME: Joi.required(),
DATABASE_PORT: Joi.number().default(5432),
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
In my case I use postgreSQL, so I will install the package pg
and @nestjs/typeorm
to connect to the database.
npm install @nestjs/typeorm typeorm pg
Then inside the app module I create import the TypeOrmModule
and configure it.
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_USER: Joi.required(),
DATABASE_PASSWORD: Joi.required(),
DATABASE_NAME: Joi.required(),
DATABASE_PORT: Joi.number().default(5432),
}),
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true,
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Configuring TypeORM migration generation
I wanted to generate migrations based on the changes I made in the entities. If I listed all the entities manually it would work, but would be a pain to maintain. So I wanted to automate this process. Based on the documentation I should be able to use a glob pattern to list all the entities.
But I don’t know if it’s a bug or if I’m doing something wrong, but it did not work and all I had was a message saying no changes were detected.
Since when I used the manual list changes were detected, but with the glob pattern it was not working, I decided to create a script that would list all the entities and generate the migration.
In the package.json I did not change anithing from the documentation and added the following scripts:
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:generate": "npm run typeorm -- migration:generate -d ./typeorm.config.ts migrations/migration",
"migration:run": "npm run typeorm -- migration:run -d ./typeorm.config.ts",
"migration:revert": "npm run typeorm -- migration:revert -d ./typeorm.config.ts",
"migration:create": "npm run typeorm -- migration:create ./migrations/migration"
}
Then I created the datasource file, and added the entities listing in it. The file is name typeorm.config.ts
import * as dotenv from 'dotenv';
import * as glob from 'glob';
import { DataSource } from 'typeorm';
dotenv.config();
async function getDatasource() {
const files = glob('src/**/entities/*.entity{.ts,.js}', { sync: true });
let entities = await Promise.all(
files.map((file) => {
return import(file);
}),
);
entities = entities.map((entity) => Object.values(entity));
entities = entities.flat();
const migrationsFiles = glob('**/migrations/*.{ts,js}', { sync: true });
let migrations = await Promise.all(
migrationsFiles.map((file) => {
return import(file);
}),
);
migrations = migrations.map((migration) => Object.values(migration));
migrations = migrations.flat();
const dataSource = new DataSource({
type: 'postgres',
host: process.env.POSTGRESQL_ADDON_HOST,
port: Number(process.env.POSTGRESQL_ADDON_PORT),
username: process.env.POSTGRESQL_ADDON_USER,
password: process.env.POSTGRESQL_ADDON_PASSWORD,
database: process.env.POSTGRESQL_ADDON_DB,
entities: [...entities],
migrations: [...migrations],
synchronize: false, // should be false in production
});
return dataSource;
}
export default getDatasource();