Nestjs🐺⚡ | The framework of Nodejs (Part-3) | Database Integration, TypeORM

Kingkor Roy Tirtho - Sep 5 '21 - - Dev Community

If you haven't read part-1 & part-2, please first read those else you'll feel this article is out of context

In this part, I'll be discussing

  • Nestjs Database integration
  • Basic Typeorm

Database Integration

A major part of a backend service/API is its Database Management System

Important: This article is about Nestjs, not about Databases (SQL/NoSQL) so I'm assuming, one reading this will have basic knowledge about DBMS

As Nestjs is only an abstraction over typical Nodejs server APIs/Package, it supports all kinds of popular databases & most of their ORMs. It supports the following database drivers & ORMs:

  • Typeorm (SQL/NoSQL )
  • MikroORM (SQL/NoSQL)
  • Knex
  • Prisma
  • mongoose (NoSQL)

I'm using TypeORM here as IMO it matches the API scheme of Nestjs the most because of its decorator pattern. Also using PostgreSQL as the Database. You can use other databases & ORMs if you want. The configuration pattern of all of them is mostly the same. Also, there's always an official/3rd-party package & documentation for your choice. Just google for it

To get started first install:

# for npm users*
$ npm i @nestjs/typeorm typeorm psql
# for yarn user
$ yarn add @nestjs/typeorm typeorm psql
Enter fullscreen mode Exit fullscreen mode

Note!: Install PostgreSQL driver or the DB driver you've chosen first in your OS. For PostgreSQL, installation instructions can be found here

Now create following files in the root of the project:

  • .env (for storing database credentials & secrets)
  • config.ts (for importing the env vars)
  • ormconfig.ts (database connection configurations)
#### .env #####
POSTGRES_PASSWORD=simplepassword
POSTGRES_DB=hello
NODE_ENV=development
DATABASE_USERNAME=postgres # you can put your username of your OS
DATABASE_HOST=localhost # use `postgres` if using PostgreSQL Docker Container
DATABASE_PORT=5432
PORT=4000
Enter fullscreen mode Exit fullscreen mode

Now import this environmental variables & re-export for the project

///// config.ts //////
export const NODE_ENV = process.env.NODE_ENV;
// all the env vars
export const DATABASE_HOST = process.env.DATABASE_HOST;
export const DATABASE_PORT = process.env.DATABASE_PORT
    ? parseInt(process.env.DATABASE_PORT)
    : undefined;
export const DATABASE_NAME = process.env.POSTGRES_DB;
export const DATABASE_PASSWORD = process.env.POSTGRES_PASSWORD;
export const DATABASE_USERNAME = process.env.DATABASE_USERNAME;
export const PORT = process.env.PORT ?? 4000;
Enter fullscreen mode Exit fullscreen mode

Create the Database connection:

///// ormconfig.ts /////
import {
    DATABASE_HOST,
    DATABASE_NAME,
    DATABASE_PASSWORD,
    DATABASE_PORT,
    DATABASE_USERNAME,
    NODE_ENV,
} from "./config";
import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";

// db configuration for the orm
const ormconfig: PostgresConnectionOptions = {
    type: "postgres", // name of db you'll be using
    username: DATABASE_USERNAME,
    database: DATABASE_NAME,
    host: DATABASE_HOST,
    port: DATABASE_PORT,
    password: DATABASE_PASSWORD,
    uuidExtension: "uuid-ossp", // for using `uuid` as the type for Primary-Column `id` column
    synchronize: NODE_ENV !== "production",
};

export = ormconfig;
Enter fullscreen mode Exit fullscreen mode

Now generate a module named database where all the database-related configuration/files will be saved. The following command will generate it:

$ npx nest g module database
Enter fullscreen mode Exit fullscreen mode

Inside database.module.ts register database connection of the configuration using TypeORM:

///// database.module.ts //////

import { Module } from '@nestjs/common';
import ormconfig from "../../ormconfig";
import { TypeOrmModule } from "@nestjs/typeorm";

@Module({
  imports: [
       // registers Database config
        TypeOrmModule.forRoot({
            ...ormconfig, //db config
            entities: [], // put the constructor of all classes that are an Entity
        }),
    ],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

Hopefully, after restarting your application, your API/service will be connected with the Database

Tip!: You can pass an Array of ORM configurations in TypeOrmModule.forRoot() function for multiple database connections. It allows using any type of database together

TypeORM

This tutorial isn't about TypeORM but I'll give a little explanation of it to get started. You can find more information about it in their docs

TypeORM API for SQL/NoSQL varies. Here I'll be showing the SQL part only. If you're using NoSQL DB e.g. MongoDB with TypeORM then you can learn it from here

If you had read part-2, you may know, there I was using a class property as an In-memory temporary database. Now we'll reflector that part to use the new PostgreSQL DB with TypeOrm

First, create src/database/entities/hello-record.entity.ts, then create a TypeORM schema:

///// hello-record.entity.ts /////

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('hello-record')
export class HelloRecord {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column('varchar', { length: 16 })
  from!: string;

  @Column('text')
  msg!: string;
}
Enter fullscreen mode Exit fullscreen mode

You can declare a class a TypeORM entity using @Entity() decorator. You can give the entity a name (which you should always do) or let typeorm create a name from the class's displayName

To create a primary column, @PrimaryGeneratedColumn is used. You can use rowId, incremented, or uuid as your primary column type. Remember to enable the UUID extensions of your PostgreSQL Database or any SQL database for using uuid

To create a Column, @Column decorator is used. You can specify the type of the column or anything of that column. I used 16 characters long varchar for column "from" because it'd be the IP address of the user who has posted a hello message. Also, I used the type text for the "msg" column as we don't want to limit anyone to only 240 characters long like some social media. That's inhumanity🤐

Now to let TypeORM know HelloRecord exists, we've to put it in the entities array of Typeorm.forRoot() function in database.module.ts. You've to put all entities that you're gonna use in the application in that array. BTW, if you're using multiple database connections, put entities, that are specifically created for the specific database in the specific database's configuration object's entities array. The same entity won't work for multiple databases

///// database.module.ts //////

// .... (other imported stuffs)
import { HelloRecord } from './entities/hello-record.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...ormconfig,
      // put all the entities related to the database in here
      entities: [
        HelloRecord,
      ],
    }),
  ],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

Important!: Never use inheritance in entity/schema declaration. If some of your entities share the same type of properties/columns, don't make a common class for them & then inherit from it. It can cause schema synchronization issues as TypeORM won't be able to know the type of that parent class's properties. Schema inheritance is not DRY instead it's an overuse of DRY (Don't Repeat Yourself). So one should never do that

Now that we've created the entity, let's use it in our HelloService. But we've to import it in HelloModule to let Nest know it belongs to HelloModule

////// hello.module.ts //////

// .... (other imported stuff)
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    forwardRef(() => HiModule),
    // labelling the entity as `HelloModule`'s Repository
    TypeOrmModule.forFeature([HelloRecord]),
  ],
  providers: [HelloService, ByeService],
  controllers: [HelloController],
  exports: [HelloService],
})
export class HelloModule {}
Enter fullscreen mode Exit fullscreen mode

TypeOrmModule.forFeature will give access to the HelloRecord entity in all providers/controllers of HelloModule. BTW, you can't label the same entity in different modules multiple times. If you want to have access to that entity in other modules, just import the provider that is using that entity

Now let's refactor HelloService to use the new Entity to save, modify & read the hello messages:

////// hello.service.ts ///////

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { HiService } from 'src/hi/hi.service';
import { ByeService } from './bye.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { HelloRecord } from '../database/entities/hello-record.entity';

@Injectable()
export class HelloService {
  constructor(
    @Inject(forwardRef(() => HiService))
    private hiService: HiService,
    @Inject(forwardRef(() => ByeService))
    private byeService: ByeService,
    @InjectRepository(HelloRecord)
    private helloRecordRepo: Repository<HelloRecord>,
  ) {}

    async findById(id: string) {
    return await this.helloRecordRepo.findOneOrFail({ id });
  }

  async create(msg: string, ip: string) {
    const newMsg = this.helloRecordRepo.create({ msg, from: ip });
    return await newMsg.save();
  }

  async deleteById(id: string) {
    return await this.helloRecordRepo.delete({ id });
  }

  getHello(arg: string) {
    return `hello for ${arg}`;
  }

  // a method that uses `hiService`
  hiServiceUsingMethod() {
    return this.hiService.getHi('hello');
  }

  byeServiceUsingMethod() {
    return this.byeService.getBye('hello');
  }

Enter fullscreen mode Exit fullscreen mode

TypeORM provides all the necessary methods to create-delete-modify data. Below are some of those that are used mostly:

  • EntityName.findOne (Finds by criteria & returns the first matched record as a Promise)
  • EntityName.findOneOrFail (Just like findOne but throws an error if no record is found. Always try using it instead of findOne as it supports error-handling)
  • EntityName.find (finds all the records matching criteria & returns as a Promise)
  • EntityName.save (saves any object passed to it matching the schema of that entity. Can be used to modify/update a record too)
  • EntityName.create (creates a new soft record which will be passed as a parameter to EntityName.save)
  • EntityName.delete (deletes all the records matching the passed criteria)
  • EntityName.createQueryBuilder (Alternative query API that uses strings to manipulate SQL transactions instead of using Object-Oriented approach. It's more like a functional approach. It follows the popular builder-pattern & supports method chaining. It's closer to native SQL)

Working final Application:


Here, I end the Nestjs🐺⚡ | The framework of Nodejs series

Nestjs is an awesome backend framework providing all the necessary tools needed for developing enterprise-grade, reliable server applications/APIs. This series is only a higher-level overview of Nestjs where very few concepts & features of Nestjs were covered. Nestjs offers much more than that. Nest's official docs provide a much deeper & clearer overview & tutorial about Nestjs. They also provide Enterprise support

Nestjs is not a tool/framework/library only, it's a guide for you too

. . . . . . . . . . . . . . . . .