4.1. Develop Full Stack Boilerplate: workspace, environment and database preparation

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.

Leave a Comment