Let's learn Node.js by building a backend with Nest.js and Sequelize - Lesson 2: User registration PART 1

Duomly - Jun 4 '20 - - Dev Community

This article was originally published at https://www.blog.duomly.com/node-js-course-with-building-a-fintech-banking-app-lesson-2-user-registration


In the previous week, I've published the first lesson of the Node.js Course, where we started a project using Nest.js, Nest CLI, PostgreSQL database, and Sequelize. Besides that, we managed to create migrations and set up the database.

So, if you would like to be updated, feel free to go back to the lesson one and follow up or get the first lesson code from our Github.

Also, if you would like to compare backend created in Node.js with backend created with GoLang, then check out my friend's Golang Course. Both are created along with the Angular 9 Course and Python and AI Course. All of them are used to build one fintech application.

Today I'm going to show you how to create user registration in Node.js.
We are going to create two modules, Users and Accounts, and we will build the functionality for creating a new user, and each new user will have a new account assigned.

And of course, as always, we've got a video version for you!

Let's start!

1. Refactor migrations

The first step will be to refactor the migrations we did in the last lesson. We have to add a few columns to the tables. So, let's run npm run migrate down twice to drop both tables. Open 1.1users.ts file and make the following changes in the code.

import * as Sequelize from 'sequelize';

const tableName = 'Users';

export async function up(i: any) {
  const queryInterface = i.getQueryInterface() as Sequelize.QueryInterface;
  queryInterface.createTable(tableName, {
    id: {
      type: Sequelize.INTEGER,
      allowNull: false,
      autoIncrement: true,
      unique: true,
      primaryKey: true,
    },
    Username: {
      type: Sequelize.CHAR(200),
      allowNull: false,
    },
    Email: {
      type: Sequelize.CHAR(50),
      allowNull: false,
    },
    Password: {
      type: Sequelize.CHAR(250),
      allowNull: false,
    },
    Salt: {
      type: Sequelize.CHAR(250),
      allowNull: true,
    },
    createdAt: {
      type: Sequelize.DATE,
    },
    updatedAt: {
      type: Sequelize.DATE,
    }
  });
};

export async function down(i: any) {
  const queryInterface = i.getQueryInterface() as Sequelize.QueryInterface;
  queryInterface.dropTable(tableName);
}
Enter fullscreen mode Exit fullscreen mode

Now, open the other migrations file 1.2accounts.ts and make sure it looks like in the code below.

import * as Sequelize from 'sequelize';

const tableName = 'Accounts';

export async function up(i: any) {
  const queryInterface = i.getQueryInterface() as Sequelize.QueryInterface;
  queryInterface.createTable(tableName, {
    id: {
      type: Sequelize.INTEGER,
      allowNull: false,
      autoIncrement: true,
      unique: true,
      primaryKey: true,
    },
    Type: {
      type: Sequelize.CHAR(200),
      allowNull: false,
    },
    Name: {
      type: Sequelize.CHAR(200),
      allowNull: false,
    },
    Balance: {
      type: Sequelize.INTEGER,
      allowNull: true,
    },
    UserId: {
      type: Sequelize.INTEGER,
      references: {
        model: 'Users',
        key: 'id',
      },
    },
    createdAt: {
      type: Sequelize.DATE,
    },
    updatedAt: {
      type: Sequelize.DATE,
    }
  });
};

export async function down(i: any) {
  const queryInterface = i.getQueryInterface() as Sequelize.QueryInterface;
  queryInterface.dropTable(tableName);
}
Enter fullscreen mode Exit fullscreen mode

The last step now is to rerun migrations, so let's use npm run migrate up and check if your database changed.

2. Install packages

To create user registration, we need some additional packages. Let's open the console and install jsonwebtoken.

$ npm install jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

And the other package we need is dotenv, to create the configuration.

$ npm install dotenv
Enter fullscreen mode Exit fullscreen mode

When it's done, let's go to the next step.

3. Create .env file

Go to the root file, create a new file, and call it .env. To this file, we are going to move database configuration.

DB_HOST=<YOUR_HOST>
DB_USER=<YOUR_USERNAME>
DB_PASS=<YOUR_PASSWORD>
DB_NAME=<YOUR_DB_NAME>
JWT_KEY=<YOUR_JWT_KEY>
Enter fullscreen mode Exit fullscreen mode

Now, we need to require this configuration in the main.ts file, and later, let's change the database configuration in the migrate.ts and database.provider.ts files.

Let's start from main.ts and import the .env.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
require('dotenv').config()

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Now, open migrate.ts and make sure it looks like this.

...
require('dotenv').config()

const sequelize = new Sequelize({
  dialect: 'postgres',
  host: process.env.DB_HOST,
  port: 5432,
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
});
...
Enter fullscreen mode Exit fullscreen mode

And finally, open the database.provider.ts file.

export const databaseProvider = [
  {
    provide: 'SEQUELIZE',
    useFactory: async () => {
      const sequelize = new Sequelize({
        dialect: 'postgres',
        host: process.env.DB_HOST,
        port: 5432,
        username: process.env.DB_USER,
        password: process.env.DB_PASS,
        database: process.env.DB_NAME,
      });
      sequelize.addModels([Users, Accounts]);
      return sequelize;
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

In the next step, we will create a JWT config.

4. JWT config

Let's go to the src folder and create a new folder called config. In that config folder, we will create a jwtConfig.ts file, and let's set options for our jwt.

export const jwtConfig = {
  algorithm: 'HS256',
  expiresIn: '1 day',
}
Enter fullscreen mode Exit fullscreen mode

Another necessary thing, we need to do right now is to generate a JWT_KEY and add it to the .env.
You can generate the key using any of the tools available on the internet or using the console command:

ssh-keygen -t rsa -b 2048 -f jwtRS256.key
Enter fullscreen mode Exit fullscreen mode

When it's ready and added to the .env file, let's got to the next step!

5. User module and entity

In this step, we are going to create a user.module.ts and we can do it manually or using Nest CLI.

$ nest generate module modules/user
Enter fullscreen mode Exit fullscreen mode

Right now, we don't need to do anything else in this file, so let's create the next file in the user folder, users.entity.ts. And inside this file, we are going to set the data we will pass to the database.

import { Table, Column, Model, DataType, CreatedAt, UpdatedAt, HasMany } from 'sequelize-typescript';
import { TableOptions } from 'sequelize-typescript';

const tableOptions: TableOptions = { timestamp: true, tableName: 'Users' } as TableOptions;

@Table(tableOptions)
export class Users extends Model<Users> {
  @Column({
    type: DataType.INTEGER,
    allowNull: false,
    autoIncrement: true,
    unique: true,
    primaryKey: true,
  })
  public id: number;

  @Column({
      type: DataType.CHAR(200),
      allowNull: false,
  })
  public Username: string;

  @Column({
    type: DataType.CHAR(50),
    allowNull: false,
    validate: {
      isEmail: true,
      isUnique: async (value: string, next: Function): Promise<any> => {
        const exists = await Users.findOne({ where: { Email: value } });
        if (exists) {
          const error = new Error('This email is already used.');
          next(error);
        }
        next();
      }
    }
  })
  public Email: string;

  @Column({
    type: DataType.CHAR(250),
    allowNull: false,
  })
  public Password: string;

  @Column({
    type: DataType.CHAR(250),
    allowNull: true,
  })
  public Salt: string;

  @CreatedAt
  public createdAt: Date;

  @UpdatedAt
  public updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Great, now we can go to the next point!

6. User provider and interface

Next, what we will do now is to create a user provider. Inside the user folder create users.provider.ts file, and inside that file, let's create the following code.

import { Users } from './users.entity';

export const UsersProviders = {
  provide: 'USERS_REPOSITORY',
  useValue: Users
}
Enter fullscreen mode Exit fullscreen mode

When it's done, let's open the module and add the provider there.

@Module({
  providers: [UsersProviders],
  exports: [
    UsersProviders,
  ]
})
Enter fullscreen mode Exit fullscreen mode

Now let's create the interface, where we will define the types of the User object.
In the user folder, create a new folder interface and in that folder, create user.interface.ts file. In this file, create the following interface.

export interface IUser {
  id: number;
  Username: string;
  Email: string;
  Password: string;
  Salt: string;
  Accounts: [];
}
Enter fullscreen mode Exit fullscreen mode

Cool, now we can go to the most exciting part of this lesson, ready?

7. User service and controller

At this point, we will create a user.service.ts file, and inside this file, we will build the function which will be saving data to the database.

Open the newly created file and type the following code.

import { Injectable, Inject } from '@nestjs/common';
import { Users } from './users.entity';
import * as jwt from 'jsonwebtoken';
import { jwtConfig } from './../../config/jwtConfig';
import crypto = require('crypto');

@Injectable()
export class UsersService { 
  constructor(
    @Inject('USERS_REPOSITORY') private usersRepository: typeof Users,
  ) { }

  public async create(user: any): Promise<object> {
    const exists = await Users.findOne({ where: { Email: user.Email } });
    if (exists) {
      throw new Error('This email is already used.');
    } else {
      user.Salt = crypto.randomBytes(128).toString('base64');
      user.Password = crypto.createHmac('sha256', user.Password + user.Salt).digest('hex');
      const newUser: any = await this.usersRepository.create<Users>(user);
      const jwtToken = jwt.sign(user, process.env.JWT_KEY, jwtConfig);
      newUser.Token = jwtToken;
      return newUser;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Great, it looks like that's it! We just need a controller right now, where we will set the endpoint and API method.

Let's create a user.controller.ts file and create the following code.

import { Controller, Post, Body, HttpException, HttpStatus } from '@nestjs/common';
import { UsersService } from './users.service';
import { IUser } from './interfaces/user.interface';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) { }

  @Post('register')  
    public async register(@Body() user: IUser): Promise<any> {    
    const result: any = await this.usersService.create(user,);
    if (!result.success) {
        throw new HttpException(result.message, HttpStatus.BAD_REQUEST);    
    }
    return result;  
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to inject those file to the module file.

@Module({
  controllers: [UsersController],
  providers: [UsersProviders, UsersService],
  exports: [
    UsersService,
    UsersProviders,
  ]
})
Enter fullscreen mode Exit fullscreen mode

It seems like the first part of our registration is ready, so let's create the second part, where we will create an account for each registered user.

8. Accounts module and entity

Let's use the Nest CLI and create a new module.

$ nest generate module modules/accounts
Enter fullscreen mode Exit fullscreen mode

You should see the new module created in a new folder, and as previously, we have nothing to do in the accounts module file right now. So, let's create the accounts.entity.ts file and make sure your file looks like the code below.

import { Table, Column, Model, DataType, CreatedAt, UpdatedAt, ForeignKey, BelongsTo } from 'sequelize-typescript';
import { TableOptions } from 'sequelize-typescript';
import { Users } from '../user/users.entity';

const tableOptions: TableOptions = { timestamp: true, tableName: 'Accounts' } as TableOptions;
@Table(tableOptions)
export class Accounts extends Model<Accounts> {
  @Column({
    type: DataType.INTEGER,
    allowNull: false,
    autoIncrement: true,
    unique: true,
    primaryKey: true,
  })
  public id: number;

  @Column({
    type: DataType.CHAR(200),
    allowNull: false,
  })
  public Type: string;

  @Column({
    type: DataType.CHAR(200),
    allowNull: false,
  })
  public Name: string;

  @Column({
    type: DataType.INTEGER,
    allowNull: true,
  })
  public Balance: number;

  @ForeignKey(() => Users)
  public UserId: number;

  @BelongsTo(() => Users, {
      as: 'Users',
      foreignKey: 'UserId',
      targetKey: 'id',
  })
  public Users: Users;

  @CreatedAt
  public createdAt: Date;

  @UpdatedAt
  public updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Great, it's ready, so we can go to the next steps.

9. Accounts provider and interface

Let's create a provider for the accounts module right now. In the accounts folder, please create the accounts.provider.ts file. In this file, you need to set the provider with the following code.

import { Accounts } from './accounts.entity';

export const AccountsProviders = {
  provide: 'ACCOUNTS_REPOSITORY',
  useValue: Accounts
};
Enter fullscreen mode Exit fullscreen mode

As previously we need an interface, so let's create the new folder called interfaces and inside that file create the accounts.interface.ts file with the following object inside.

export interface IAccount {
  id: number;
  Type: string;
  Name: string;
  Balance: number;
  UserId: number;
}
Enter fullscreen mode Exit fullscreen mode

We are ready to create AccountsService and controller.

10. Accounts service and controller

In the accounts folder, let's create an accounts.service.ts file, and in this file, you need to create the following function.

import { Injectable, Inject } from '@nestjs/common';
import { Accounts } from './accounts.entity';

@Injectable()
export class AccountsService { 
  constructor(
    @Inject('ACCOUNTS_REPOSITORY')
    private accountsRepository: typeof Accounts
  ) { }

  public async create(UserId: number): Promise<object> {
    const account = {
      Name: 'Account',
      Type: 'Personal Account',
      Balance: 100, 
      UserId: UserId,
    }
    const newAccount: any = await this.accountsRepository.create<Accounts>(account);
    return newAccount;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are setting the hardcoded values for our initial user account as it will be the default one, and later user will be able to make changes.

Let's create an accounts.controller.ts file in the same folder. And in that file type the following code, so we will be able to use it from the endpoint as well.

import { AccountsService } from './accounts.service';
import { Controller, Post, Body, HttpException, HttpStatus } from '@nestjs/common';
import { IAccount } from './interfaces/accounts.interface';

@Controller('accounts')
export class AccountsController {
  constructor(private accountsService: AccountsService) { }

  @Post('create-account')  
    public async register(@Body() UserId: number): Promise<any> {    
    const result: any = await this.accountsService.create(UserId);
    if (!result.success) {
      throw new HttpException(result.message, HttpStatus.BAD_REQUEST);    
    }
    return result;  
  }
}
Enter fullscreen mode Exit fullscreen mode

We are almost at the end of the lesson. We just need to update a few files and test it.

11. Add AccountsService to UserService

We will be using the function to create an account in our register function. But first, we need to update modules, so let's open accounts.module.ts file and make sure it looks like the code below.

@Module({
  imports: [DatabaseModule],
  controllers: [AccountsController],
  providers: [AccountsProviders, AccountsService],
  exports: [AccountsProviders, AccountsService]
})
Enter fullscreen mode Exit fullscreen mode

When it's saved, open the other module file users.module.ts and update it as well.

@Module({
  controllers: [UsersController],
  imports: [AccountsModule],
  providers: [UsersProviders, UsersService],
  exports: [
    UsersService,
    UsersProviders,
  ]
})
Enter fullscreen mode Exit fullscreen mode

So, we can import it in the user.service.ts. The file should look like the code below now.

import { Injectable, Inject } from '@nestjs/common';
import { Users } from './users.entity';
import * as jwt from 'jsonwebtoken';
import { jwtConfig } from './../../config/jwtConfig';
import { AccountsService } from './../accounts/accounts.service';
import crypto = require('crypto');

@Injectable()
export class UsersService { 
  constructor(
    @Inject('USERS_REPOSITORY') private usersRepository: typeof Users,
    private accountsService: AccountsService,
  ) { }

  public async create(user: any): Promise<object> {
    const exists = await Users.findOne({ where: { Email: user.Email } });
    if (exists) {
      throw new Error('This email is already used.');
    } else {
      user.Salt = crypto.randomBytes(128).toString('base64');
      user.Password = crypto.createHmac('sha256', user.Password + user.S
alt).digest('hex');
      const newUser: any = await this.usersRepository.create<Users>(user);
      const jwtToken = jwt.sign(user, process.env.JWT_KEY, jwtConfig);
      newUser.Token = jwtToken;
      if (newUser) {
        this.accountsService.create(newUser.id)
      }
      return newUser;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, we've passed the create function from accountsService and created a new account each time the new user registers.

To make the whole backend working, we need to update the user entity and database provider. Let's open the user.entity.ts file, and we will add a few lines of code at the end of the User class.

  @HasMany(() => Accounts, 'UserId')
  public Accounts: Accounts[];
Enter fullscreen mode Exit fullscreen mode

Now, let's open database.provider.ts file and import both entities. When the import is done, inject them as models.

import { Sequelize } from 'sequelize-typescript'; 
import { Users } from '../user/users.entity'; 
import { Accounts } from '../accounts/accounts.entity'; 
export const databaseProvider = [ 
  { 
      provide: 'SEQUELIZE', 
      useFactory: async () => { 
         const sequelize = new Sequelize({ 
            dialect: 'postgres', 
            host: process.env.DB_HOST, 
            port: 5432, 
            username: process.env.DB_USER, 
            password: process.env.DB_PASS, 
            database: process.env.DB_NAME, 
         }); 
     sequelize.addModels([Users, Accounts]); 
     return sequelize; 
    } 
  }
]
Enter fullscreen mode Exit fullscreen mode

And voila! Let's test it!

12. Testing

I'll be using the Postman to test our API right now. If you are not running the app yet, please do it using nest start or npm run start, and when it's ready to open the Postman. In the image below, you can see my setting, so you can try similar. Also, you can open your database to see if the data is there.

Duomly - Node.js Course

I hope it works for you as well!

Conclusion

In this lesson, we build the registration of a new user and creating the default account.
In the next lessons, we are going to work on the login feature.
If you didn't manage to get all the code correctly, jump into our Github and find your bugs.

Node.js Course - Lesson 2: User registration - Code

Also, remember to jump into other courses where we are building GoLang backend for the same app, AI for investment, and frontend with Angular 9.

Thank you for reading,
Anna from Duomly


Duomly - Programming Online Courses

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