Enterprise-ready TypeScript Nest.js API with TypeORM

Enterprise-ready TypeScript Nest.js API with TypeORM

In the previous article, I have described how to create API in Rust using few different API frameworks. Today let’s see how to create Nest.js API with TypeORM and MySQL. I’m going to create the same User API to be able to compare different languages in one of the future articles. Let’s start.

Nest.js API

Nest.js is an MVC framework for Node.js. This is my goto when I think about scalable, enterprise architecture in Javascript/Typescript. It can run on the Express server or use Fastify. I will show you how to implement both in this project. It can be also deployed so AWS Lambda. This will come handy in the future to test all API’s in the AWS.

Setting up Nest.js project

To create new project in Nest.js just install Nest CLI. You need to have Node.js 10.13.0 or later already on your system.

npm i -g @nestjs/cli
nest new user-nest

It will scaffold a new project with a module, controller and main file. The controller is where we define REST actions. Module is the file where we define object relationships and dependencies. Services, repositories are called providers. To be able to use it, it has to be defined in the module. Main is our entry point to the application where we can start Express or Fastify server.

Let’s delete the app.controller and app.module and create a new structure for our User API. In theory, there is no need to do that for this example as you will have only User API, but when your project is growing it is good to have modules separated.

I have created an API folder where controllers and DTO’s will sit. Also, let’s create a core folder – for models and services and a shared folder for database settings and environment variables access. Check code on my Github to see it in detail. 

Dependencies – TypeOrm and more.

Let’s add all the required dependencies for this project with npm:

npm -i --save [name_of_the_package]
npm -i --save-dev [name_of_the_package]

Let’s check package.json for details.

"dependencies": {
    "@nestjs/common": "^6.7.2",
    "@nestjs/core": "^6.7.2",
    "@nestjs/platform-express": "^6.11.11",
    "@nestjs/platform-fastify": "^7.0.5",
    "@nestjs/typeorm": "^7.0.0",
    "aws-serverless-express": "^3.3.6",
    "class-transformer": "^0.2.3",
    "class-validator": "^0.11.1",
    "mysql": "^2.18.1",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.0",
    "rxjs": "^6.5.3",
    "typeorm": "^0.2.24"
  },
  "devDependencies": {
    "@nestjs/cli": "^6.9.0",
    "@nestjs/schematics": "^6.7.0",
    "@nestjs/swagger": "^4.4.0",
    "@nestjs/testing": "^6.7.1",
    "@types/express": "^4.17.1",
    "@types/jest": "^24.0.18",
    "@types/node": "^12.7.5",
    "@types/supertest": "^2.0.8",
    "jest": "^24.9.0",
    "nodemon": "^2.0.2",
    "prettier": "^1.18.2",
    "supertest": "^4.0.2",
    "ts-jest": "^24.1.0",
    "ts-loader": "^6.1.1",
    "ts-node": "^8.4.1",
    "tsconfig-paths": "^3.9.0",
    "tslint": "^5.20.0",
    "typescript": "^3.6.3"
  },

I’m going to add MySQL, TypeORM packages to handle the database. Platform-express and platform-fastify will enable to test on both servers. Class transformer and class validator are useful to define API fields and validate types.

For better debugging, I’m using nodemon with nodemon-debug.json config. By adding this line to package.json I can quickly run this app by executing

npm run start:dev

package.json:

"start:dev": "export ENVIRONMENT=local; nodemon --config nodemon-debug.json",

Shared

In the shared folder let’s configure database access and access to environment variables. It is also a good place for logging but I will show it n the future.

Access to variables:

class EnvironmentVariables {

    public env = process.env.NODE_ENV || process.env.ENVIRONMENT || 'prod';

    get isDev(): boolean {
        return !this.isProd;
    }
    get isProd(): boolean {
        return this.env === 'production' || this.env === 'prod';
    }

    get IS_OFFLINE(): boolean {
        return process.env.IS_OFFLINE === 'true' || process.env.ENVIRONMENT === 'local';
    }

    get MYSQL_HOST(): string {
        return process.env.MYSQL_HOST || 'localhost'
    }

    get MYSQL_USER(): string {
        return process.env.MYSQL_USER || 'root'
    }

    get MYSQL_PASSWORD(): string {
        return process.env.MYSQL_PASSWORD || 'root'
    }

    get MYSQL_DATABASE(): string {
        return process.env.MYSQL_DATABASE || 'slicewall'
    }

    get MYSQL_PORT(): number {
        return parseInt(process.env.MYSQL_PORT, 10) || 3306
    }
}

export default new EnvironmentVariables();

Database config:

import { Injectable } from '@nestjs/common';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { ConnectionManager, getConnectionManager } from 'typeorm';
import { User } from '../core/user/model/user.entity';
import env_variables from "./env_variables";


@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
    async createTypeOrmOptions(): Promise<TypeOrmModuleOptions> {
        const connectionManager: ConnectionManager = getConnectionManager();
        let options: any;

        if (connectionManager.has('default')) {
            options = connectionManager.get('default').options;
            await connectionManager.get('default').close();
        } else {
            options = {
                name: 'default',
                type: 'mysql',
                host: env_variables.MYSQL_HOST,
                username: env_variables.MYSQL_USER,
                password: env_variables.MYSQL_PASSWORD,
                database: env_variables.MYSQL_DATABASE,
                port: env_variables.MYSQL_PORT,
                //entities: [__dirname + '/../modules/*/model/**.entity{.ts,.js}'],
                entities: [User],
                synchronize: false,
                logging: false,
                extra: {
                    charset: "utf8mb4_unicode_ci"
                }

            } as TypeOrmModuleOptions;
        }
        return options;
    }
}

You need to provide a list of entities to TypeORM. For docker/server API it can be auto imported by searching the path (commented out ). For Lambda functions it was not working for me and I had to manually provide all entities. Synchronize is set to false so my database will not be altered in case of changes in models. If you are using a dedicated database for your project it is a very nice feature that saves a lot of time. The @Injectable annotation tells Nest.js that this class can be used by dependency injection.

Core/user/model

In model directory let’s create our user and connect it with TypeORM.

import { Entity, PrimaryColumn, Column, CreateDateColumn } from "typeorm";


@Entity()
export class User {

    @PrimaryColumn()
    id: string;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column({ unique: true })
    email: string;

    @Column()
    name: string;

    @CreateDateColumn({ type: 'timestamp' })
    createAt: Date;

    @Column({ nullable: true })
    phoneNo: string

    @Column({ nullable: true })
    companyName: string

    @Column({ nullable: true })
    vatId: string

}

Core/user – service

Now it’s time for user service from where we can grab database data. Here you can use Nest.js repository injection that is available out of the box from TypeOrm.

import { Injectable, HttpException, HttpStatus } from "@nestjs/common";
import { User } from "./model/user.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository, TransactionRepository, Transaction } from "typeorm";



@Injectable()
export class UserService {


    constructor(@InjectRepository(User)
    private readonly userRepository: Repository<User>) {

    }


    async get(): Promise<User[]> {

        try {
            return await this.userRepository.find();

        } catch (error) {
            //logger.error('core.user.userService.get', { data: { id, error: error } })
        }


    }


    getNoDB(): User {

        try {

            const _users_json: User = JSON.parse(`{"id":"89251ab3-1cdc-4629-9086-ce022cf6669e","firstName":"Marek","lastName":"Majdak","email":"info@sufrago.com","name":"sufrago","createAt":"2019-12-17T17:58:07.533406","phoneNo":"+48666666666","companyName":"Sufrago sp z o.o.","vatId":"PL 9512468001"}`);
            return _users_json;

        } catch (error) {
            //logger.error('core.user.userService.get', { data: { id, error: error } })
        }


    }

}

We have two methods. One to test getting the user from DB and another one for testing API without database. Just returns deserialized JSON.

Api/user/dto

In this example, our DTO will match the model but in real case scenario, you can return a different view of data for different API calls

import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEmail, IsOptional, IsUUID, IsAlpha, IsPhoneNumber, IsAlphanumeric } from 'class-validator';
import { Expose, plainToClass } from 'class-transformer';
import { User } from '../../../core/user/model/user.entity';


export class UserDto implements Readonly<UserDto> {

    @ApiProperty({ required: true })
    @IsUUID()
    @Expose()
    id: string;

    @ApiProperty({ required: true })
    @IsAlpha()
    @IsString()
    @Expose()
    firstName: string;

    @ApiProperty({ required: true })
    @IsString()
    @Expose()
    lastName: string;

    @ApiProperty({ required: true })
    @IsEmail()
    @Expose()
    email: string;

    @ApiProperty({ required: true })
    @IsString()
    @IsAlphanumeric()
    @Expose()
    name: string;

    @ApiProperty()
    @IsOptional()
    @IsString()
    @IsPhoneNumber('ZZ')
    @Expose()
    phoneNo: string;

    @ApiProperty()
    @IsOptional()
    @IsString()
    @Expose()
    companyName: string;

    @ApiProperty()
    @IsOptional()
    @IsString()
    @Expose()
    vatId: string;



    public static from(dto: Partial<UserDto>) {

        return plainToClass<UserDto, Partial<UserDto>>(UserDto, dto, { excludeExtraneousValues: true })
    }

    public static fromEntity(entity: User) {
        return plainToClass<UserDto, Partial<User>>(UserDto, entity, { excludeExtraneousValues: true })
    }

    public toEntity() {

        return plainToClass<User, Partial<UserDto>>(User, this, { excludeExtraneousValues: true })
    }

}

By using class validator and class transformer you can make sure that data is in the correct format when sending it to/from the client.

Api/user – module

In this Nest.js specific class, we can tell what features we want to use in this module and configure TypeOrm with User entity.

import { User } from "../../core/user/model/user.entity";
import { Module } from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserService } from "../../core/user/user.service";
import { UserController } from "./user.controller";


@Module({
    imports: [TypeOrmModule.forFeature([User])],
    providers: [UserService],
    controllers: [UserController],
    exports: [UserService],
})
export class UserModule { }

Api/user – controller

In the controller, I’m going to set up REST paths and use our services to get data.

import { ApiTags, ApiOperation, ApiParam } from "@nestjs/swagger";
import { Controller, Get } from "@nestjs/common";
import { UserService } from "../../core/user/user.service";
import { UserDto } from "./dto/user.dto";




@ApiTags('User')
@Controller()
export class UserController {

    constructor(public readonly service: UserService) {

    }


    @ApiOperation({ description: 'Get all users from DB' })
    @Get()
    async getOne(): Promise<UserDto[]> {
        return await this.service.get().then(r => r.map(e => UserDto.fromEntity(e)));
    }


    @ApiOperation({ description: 'Get user from JSON })
    @Get('nodb')
    getNoDb(): UserDto {
        return UserDto.fromEntity(this.service.getNoDB());
    }

}

The ApiOeration is only for Swagger, but it is not configured for this project.

Module

In the main app module, I’m going to ser TypeOrm database configuration and add our User module. This is the place where you can set up middleware for your API.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './shared/mysql.config';
import { UserModule } from './api/user/user.module';

@Module({
  imports: [
    TypeOrmModule.forRootAsync(
      {
        //inject: [ConfigModule],
        useClass: TypeOrmConfigService,
      }),
    UserModule
  ],
  controllers: [],
  providers: [],
})
export class AppModule { }

Express and Fastify

I’m going to create two bootstrap methods to run API with the selected server.

For Express server it looks like this:

import { Server } from 'http';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as serverless from 'aws-serverless-express';
import { INestApplication } from '@nestjs/common';


export async function bootstrap() {
    const expressApp = require('express')();
    const adapter = new ExpressAdapter(expressApp);

    const nestApp = await NestFactory.create(AppModule, adapter);

    nestApp.enableCors();
    await nestApp.listen(8088);
}

For Fastify:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
    FastifyAdapter,
    NestFastifyApplication,
} from '@nestjs/platform-fastify';
import * as fastify from 'fastify';


export async function bootstrap() {
    const serverOptions: fastify.ServerOptionsAsHttp = {
        logger: false,
    };
    const instance: fastify.FastifyInstance = fastify(serverOptions);
    const nestApp = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        new FastifyAdapter(instance),
    );

    nestApp.enableCors();

    await nestApp.listen(8088);
}

Now in the main.ts, you can run your API by calling ONE of the bootstrap methods.

 import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { bootstrap as express } from './app.express';
import { bootstrap as fastify } from './app.fastify';


//express();
fastify();

And that is it. You have a fully working Nest.js API in TypeScript.

In the next article, I will demonstrate how to create User API in .Net Core 3.1. After that, I think it will be a good time to get all the results together and compare speed between Node.js, Rust and .Net Core. Make sure to visit us soon for more articles.

Github – https://github.com/marekm1844/user-nestjs

If you have an interesting project or need a highly qualified team, take a look at Sufrago.com to learn more about our company and get in touch.

Leave a Reply

Your email address will not be published. Required fields are marked *