The previous articles demonstrated the basic prep work to start writing code. And now we can start! This article shows the execution of the first task of the first stage: “Create workspace, set up environment and migrations on backend“
Check installed globally versions of NodeJS, NX, Angular, NestJS and update or install it
Generate nx workspace with nestjs preset:
npx create-nx-workspace@latest --preset=nest --name=atcs-demo --appName=backend
After the successfully created the workspace: atcs-demo go to this folder in the terminal
Install Angular to workspace:
npm install -D @nx/angular
Generate angular app called frontend
nx generate @nx/angular:application --name frontend --style scss --prefix nav --tags type:app,scope:client --strict --backendProject backend --routin [/code] with options:
with options:
Which bundler do you want to use to build the application? · esbuild Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? (y/N) · false What should be the project name and where should it be generated? · frontend @ apps/frontend
Run and check backend and frontend apps
nx run-many -t serve -p backend frontend --parallel
Add to git and create first commit: “created nx workspace with nest preset called backend, added angular app called frontend, installed jest”
Create PostgreSQL database for project
Create .env file
DB_HOST=db DB_TYPE=postgres DB_NAME=atcs-demo DB_PORT=5432 DB_USER_NAME=user DB_USER_PASSWORD=post PGADMIN_PORT=5080 PGADMIN_DEFAULT_EMAIL=pgadmin4@pgadmin.org PGADMIN_DEFAULT_PASSWORD=admin API_PORT=3003
Create docker-compose.yml
version: '3.9'
services:
db:
image: postgres:14.2
container_name: atcs-db
hostname: ${DB_HOST}
ports:
- ${DB_PORT}:5432
environment:
POSTGRES_USER: ${DB_USER_NAME}
POSTGRES_PASSWORD: ${DB_USER_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- postgres-data:/var/lib/postgresql/data
restart: always
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER_NAME} -d ${DB_NAME} -h ${DB_HOST} -p ${DB_PORT}"]
interval: 30s
timeout: 5s
retries: 3
pgadmin:
image: dpage/pgadmin4
container_name: atcs-db-admin
depends_on:
- db
env_file:
- .env
ports:
- ${PGADMIN_PORT}:80
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
PGADMIN_LISTEN_PORT: 80
volumes:
- pgadmin-data:/var/lib/pgadmin
restart: always
networks:
- backend
volumes:
postgres-data:
pgadmin-data:
networks:
backend:
driver: bridge
Add scripts to package.json:
"scripts": {
"db:start": "docker-compose up -d",
"db:stop": "docker-compose stop",
}
db:start script – create docker container for our database called atcs-db-admin
db:stop – stop docker container atcs-db-admin for our database
Run script “db:start” and check container and database
Prepare migration / Create first Entity to set migrations
Generate domain library to store data models
npx nx generate @nx/js:library domain --directory=libs/shared --importPath=@mtfs/shared/domain --skipBabelrc --tags=scope:shared,type:domain
Create base interface libs/shared/domain/src/lib/base.interface.ts. This interface will be parent for all interfaces of data models
export interface IBase {
id: string;
createdAt: Date;
updatedAt: Date;
}
Create user interface libs/shared/domain/src/lib/user.interface.ts
import { IBase } from "./base.interface";
export interface IUser extends IBase{
email: string;
password: string | undefined; //undefined
phoneNumber: string;
firstname: string;
lastname: string;
}
Generate nestjs backend features library:
npx nx generate @nx/nest:library features \ --directory=libs/backend/features \ --importPath=@mtsf/backend/features \ --strict --tags=type:features,scope:backend \ --projectNameAndRootFormat=as-provided \ --buildable=true \ --unitTestRunner=jest
Add nestjs/config dependency
npm i --save @nestjs/config
Create libs/backend/resources/src/lib/database/orm.config.ts – to set configuration for TypeORM:
import { ConfigService } from "@nestjs/config";
import { config } from "dotenv";
import { DataSource, DataSourceOptions } from "typeorm";
config();
const configService = new ConfigService();
export const dataSourceOptions: DataSourceOptions = {
type: 'postgres',
host: configService.get('DB_HOST'),
port: Number(configService.get('DB_PORT')),
username: configService.get('DB_USER_NAME'),
password: configService.get('DB_USER_PASSWORD'),
database: configService.get('DB_NAME'),
synchronize: false,
migrationsTableName: 'migrations',
entities: [
'dist/**/entities/*.entity{.ts,.js}'
],
migrations: [
'dist/libs/backend/resources/src/lib/database/migrations/*{.ts,.js}',
]
}
export const dataSource = new DataSource(dataSourceOptions);
dataSource.initialize()
.then(() => console.log("Data Source has been initialized"))
.catch((error) => console.log("Error initializating Data Source", error))
Update AppModule (apps/backend/src/app/app.module.ts) to setup ConfigModule, TypeOrmModule and add ResourceModule:
import { Module } from '@nestjs/common';
import {ConfigModule} from '@nestjs/config';
import {TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {FeaturesModule, dataSourceOptions} from '@mtfs/backend/features'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env'],
ignoreEnvVars: true,
}),
TypeOrmModule.forRootAsync({
useFactory: () => ({
...dataSourceOptions,
autoLoadEntities: false,
})
}),
FeaturesModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Generate first nestjs resource users in features library:
nx g @nx/nest:resource users \ --directory=libs/backend/features/src/lib/users \ --crud=true \ --type=rest
Install Swagger to add API specifications during development process
npm install --save @nestjs/swagger
Create base entity libs/backend/resources/src/lib/base-entities/base.entity.ts:
import {IBase} from '@mtfs/shared/domain';
import { ApiProperty, ApiTags } from '@nestjs/swagger';
import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@ApiTags('base entity')
export class Base implements IBase{
@ApiProperty({
type: String,
readOnly: true,
description: 'The unique identifier of the entity.',
example: 'DCA76BCC-F6CD-4211-A9F5-CD4E24381EC8',
})
@PrimaryGeneratedColumn('uuid')
id!: string;
@ApiProperty({
type: Date,
example: 'datetime',
description: 'The date and time when the entity was created.' })
@CreateDateColumn()
createdAt!: Date;
@ApiProperty({
type: Date,
example: 'datetime',
description: 'The date and time when the entity was last updated.' })
@UpdateDateColumn()
updatedAt!: Date;
}
Create user entity libs/backend/resources/src/lib/users/entities/user.entity.ts:
import { Column, Entity, } from "typeorm";
import {IUser} from '@mtfs/shared/domain';
import { Base } from "../../base-entity/base.entity";
@Entity('users')
export class User extends Base implements IUser {
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
email!: string;
@Column({ type: 'varchar', length: 14, nullable: false })
phoneNumber!: string;
@Column({ type: 'varchar', select: false, nullable: false })
password!: string;
@Column({ type: 'varchar', length: 120, nullable: false })
firstname!: string;
@Column({ type: 'varchar', length: 120, nullable: false })
lastname!: string;
}
Update UsersModule libs/backend/resources/src/lib/users/users.module.ts to set up TypeORM
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Update FeaturesModule libs/backend/features/src/lib/features.module.ts adding UsersModule to exports and imports:
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
controllers: [],
providers: [],
exports: [UsersModule],
imports: [UsersModule],
})
export class FeaturesModule {}
Now almost everything is ready to create migration scripts and test the performance of migrations. “Almost” because there are one huck to to work migrations properly in the NX workspace when using the NestJS and TypeORM stack:
First, we have to change executor and some options in apps/backend/project.json:
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/backend",
"main": "apps/backend/src/main.ts",
"tsConfig": "apps/backend/tsconfig.app.json",
"assets": ["apps/backend/src/assets"]
},
....
and second change compiler options in apps/backend/tsconfig.app.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"module": "commonjs",
"types": ["node"],
"target": "es2021",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
And finally we create migration scripts in package.json:
"typeorm": "typeorm-ts-node-commonjs -d libs/backend/features/src/lib/database/orm.config.ts", "db:migration:create": "npx typeorm-ts-node-commonjs migration:create libs/backend/features/src/lib/database/migrations/created_migration --timestamp", "db:migration:generate": "nx build backend && npm run typeorm migration:generate libs/backend/features/src/lib/database/migrations/generated_migration --timestamp", "db:migration:run": "nx build backend && npm run typeorm migration:run", "db:migration:revert": "npm run typeorm migration:revert", "db:migration:show": "npm run typeorm migration:show",
Test migration
To create first migration we run in terminal script: “db:migration:create ”
This script generates first migration file in libs/backend/features/src/lib/database/migrations folder:
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreatedMigration1715350334800 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
Than we run script: “db:migration:run”
This script generates “migrations” table in our database “atcs-demo” with record of our migration filename: “CreatedMigration1715350334800”
Than we run script: “db:migration:generate” to generate next migration file:
import { MigrationInterface, QueryRunner } from "typeorm";
export class GeneratedMigration1715350533935 implements MigrationInterface {
name = 'GeneratedMigration1715350533935'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "email" character varying(255) NOT NULL, "phoneNumber" character varying(14) NOT NULL, "password" character varying NOT NULL, "firstname" character varying(120) NOT NULL, "lastname" character varying(120) NOT NULL, CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE "email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "users"`);
}
}
And finally we run script “db:migration:generate” again. This script generates first table “users” in “atcs-demo” database.
We can use script “db:migration:revert” to revert last migration at any time and make corrections of our data models.
And the last thing of prepare our environment for further development is update apps/backend/src/main.ts by adding swagger setup to generate api documentation:
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const configService = app.get(ConfigService);
const port = configService.get('API_PORT') || 3003;
//setup swagger
const config = new DocumentBuilder()
.setTitle('atcs-demo')
.setVersion('1.0')
.addCookieAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/v1', app, document);
await app.listen(port);
Logger.log(
` Application is running on: http://localhost:${port}/${globalPrefix}`
);
}
bootstrap();
Summary and conclusions
The previous articles have laid the groundwork to begin the coding process, and we’re finally getting started! This article focuses on completing the initial task of the first stage of our project: “Create workspace, set up environment and migrations on backend“: As result we have NX monorepository with NestJS on backend and Angular on frontend, PostgreSQL database in Docker container and prepared migrations to be ready develop our database and backend api. In the next article will be discovered implementation of the next task: “Create backend authentication” using cookie authenticatition with Passport, JWT and bcrypt.
Discover more from More Than Fullstack
Subscribe to get the latest posts sent to your email.