DEV Community

Cover image for Part 1: Create an API with Nest.js [2024]
Ramon Serrano
Ramon Serrano

Posted on

Part 1: Create an API with Nest.js [2024]

Repository

https://github.com/RamEduard/nestjs-expenses-rest-api

Introduction


The following post will be a tutorial for creating an Expenses and Incomes REST API build using NestJS.

This a personal project to manage my personal finances, and in the same process apply concepts of Software Engineering.

Idea of Part 2

The idea for a Part 2 is to develop the User Interface using an Open Source template already designed on React or Angular.

Modules


This API will have the following modules:

Account

This module will be to represent Bank Accounts, Wallets, Cash, etc.

Category

This module is for classifying the Records by category (Food, House repairs, Taxes, etc).

Record

For saving records related to movements (expense/income transactions)

User

For saving information related to a user

User could be anonymous.

It should exists a mechanism to relate the records to a unique user id in case that is anonymous.

Authorization Module

Other modules your could add

  • Alerts or reminders based on Records
  • Reports

Installations


Consider that this project will be developed and tested on:

  • Macbook Pro (Apple M2)
  • MacOS version: Sonoma 14.5

Needed

ℹ️ Bun
This tutorial we will use this runtime to install and run the node commands from package.json
https://bun.sh/

Clone the TypeScript stater project


git clone https://github.com/nestjs/typescript-starter.git nestjs-expenses-rest-api
cd nestjs-expenses-rest-api
bun install
bun start
Enter fullscreen mode Exit fullscreen mode

Now, open http://localhost:3000/ on your browser and you will see the message Hello World!

Git - Reinitialize the project

Execute the following to reinitialize .git folder

rm -rf .git
git init
echo 'bun.lockb' >> .gitignore
git add .
git commit -m "NestJS typescript starter boilerplate"
Enter fullscreen mode Exit fullscreen mode

Create a new repository on your GitHub, GitLab, Bitbucket, or another of your preference, and replace the remote origin with the following command:

git remote rm origin
git remote add origin git@github:username/nestjs-expenses-rest-api.git
git push origin main
Enter fullscreen mode Exit fullscreen mode

Access rights or repo does not exist error

⚠️ I got the following error: 'Please make sure you have the correct access rights
and the repository exists'
, when trying to push my changes on my repository. So I suggest to take the following notes in consideration:

  • Configure your SSH keys on your remote git tool (GitHub, GitLab, Bitbucket)
  • Run eval "$(ssh-agent -s)" , after ssh-add and then try again git fetch origin

Database configuration


For this section we will follow the instructions on the official NestJS documentation for Database technique.

We will use TypeORM Technique provided by NestJS.

Install dependencies

bun install @nestjs/typeorm typeorm mysql2 --save
Enter fullscreen mode Exit fullscreen mode

Create a DatabaseModule

We are going to create a DatabaseModule where we will configure TypeORM using TypeOrmModule.

Create the database module folder and file:

mkdir -p src/modules/database
touch src/modules/database/database.module.ts
Enter fullscreen mode Exit fullscreen mode

Then add the following content to src/modules/database/database.module.ts :

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'nestjs-expenses-dev',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

ℹ️ In future sections we will see how to configure this module to include different environments (dev, test, live).

⚠️ WARNING
Setting synchronize: true shouldn't be used in production - otherwise you can lose production data.

Now we can import DatabaseModule on AppModule , and src/app.module.ts will be like this:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './modules/database/database.module';

@Module({
  imports: [DatabaseModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

When you execute bun start on your terminal it will fail with the following error:

ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
Enter fullscreen mode Exit fullscreen mode

But don’t worry, the next step you will need to do is to create the database on your local machine.

Configure mysql


Docker

You could run an instance of mysql using docker. Check this link for more information about docker mysql.

The following command can create the mysql instance on docker.

ℹ️ Use sudo on Linux

docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 mysql
Enter fullscreen mode Exit fullscreen mode

You must change the password for this user. Use the following command:

docker exec -it mysql mysql -uroot -p
Enter fullscreen mode Exit fullscreen mode

Type your password, press enter, and you will see the following:

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 26
Server version: 8.0.32

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
Enter fullscreen mode Exit fullscreen mode

Create the database, for example:

CREATE DATABASE nestjs_expenses_dev;
Enter fullscreen mode Exit fullscreen mode

Then exit from mysql CLI:

exit
Enter fullscreen mode Exit fullscreen mode

Next step is to update src/modules/database/database.module.ts with the new values database and password.

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'nestjs_expenses_dev',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

Test now your connection running:

bun start
Enter fullscreen mode Exit fullscreen mode

And you will see the following log on your console:

➜  nestjs-expenses-rest-api git:(main) ✗ bun start
$ nest start
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [NestFactory] Starting Nest application...
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [InstanceLoader] DatabaseModule dependencies initialized +40ms
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +33ms
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [RoutesResolver] AppController {/}: +11ms
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 54511  - 07/03/2024, 5:52:59 PM     LOG [NestApplication] Nest application successfully started +1ms
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add .
git commit -m "DatabaseModule added"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Configure sqlite

TODO

Create Skeleton of Modules


On NestJS we can use the CLI to create controllers, services, entities, modules, etc. So we will execute some commands to add these modules.

Accounts Module

npx nest generate resource accounts
Enter fullscreen mode Exit fullscreen mode

The prompt will ask you:

  • ? What transport layer do you use? REST API
  • ? Would you like to generate CRUD entry points? (Y/n) Y
? What transport layer do you use? (Use arrow keys)
❯ REST API 
  GraphQL (code first) 
  GraphQL (schema first) 
  Microservice (non-HTTP) 
  WebSockets
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/accounts/accounts.controller.spec.ts (596 bytes)
CREATE src/accounts/accounts.controller.ts (957 bytes)
CREATE src/accounts/accounts.module.ts (269 bytes)
CREATE src/accounts/accounts.service.spec.ts (474 bytes)
CREATE src/accounts/accounts.service.ts (651 bytes)
CREATE src/accounts/dto/create-account.dto.ts (33 bytes)
CREATE src/accounts/dto/update-account.dto.ts (181 bytes)
CREATE src/accounts/entities/account.entity.ts (24 bytes)
UPDATE package.json (2153 bytes)
UPDATE src/app.module.ts (409 bytes)
✔ Packages installed successfully.
Enter fullscreen mode Exit fullscreen mode

Categories Module

npx nest generate resource categories
Enter fullscreen mode Exit fullscreen mode

Result:

? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/categories/categories.controller.spec.ts (616 bytes)
CREATE src/categories/categories.controller.ts (989 bytes)
CREATE src/categories/categories.module.ts (283 bytes)
CREATE src/categories/categories.service.spec.ts (488 bytes)
CREATE src/categories/categories.service.ts (667 bytes)
CREATE src/categories/dto/create-category.dto.ts (34 bytes)
CREATE src/categories/dto/update-category.dto.ts (185 bytes)
CREATE src/categories/entities/category.entity.ts (25 bytes)
UPDATE src/app.module.ts (494 bytes)
Enter fullscreen mode Exit fullscreen mode

Records Module

npx nest generate resource records
Enter fullscreen mode Exit fullscreen mode

Result:

? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/records/records.controller.spec.ts (586 bytes)
CREATE src/records/records.controller.ts (936 bytes)
CREATE src/records/records.module.ts (262 bytes)
CREATE src/records/records.service.spec.ts (467 bytes)
CREATE src/records/records.service.ts (637 bytes)
CREATE src/records/dto/create-record.dto.ts (32 bytes)
CREATE src/records/dto/update-record.dto.ts (177 bytes)
CREATE src/records/entities/record.entity.ts (23 bytes)
UPDATE src/app.module.ts (567 bytes)
Enter fullscreen mode Exit fullscreen mode

Users Module

npx nest generate resource users
Enter fullscreen mode Exit fullscreen mode

Result:

? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/users/users.controller.spec.ts (566 bytes)
CREATE src/users/users.controller.ts (894 bytes)
CREATE src/users/users.module.ts (248 bytes)
CREATE src/users/users.service.spec.ts (453 bytes)
CREATE src/users/users.service.ts (609 bytes)
CREATE src/users/dto/create-user.dto.ts (30 bytes)
CREATE src/users/dto/update-user.dto.ts (169 bytes)
CREATE src/users/entities/user.entity.ts (21 bytes)
UPDATE src/app.module.ts (632 bytes)
Enter fullscreen mode Exit fullscreen mode

Move created modules to src/modules

Let move this folder from src/ to src/modules/ .

mv src/accounts src/modules/
mv src/categories src/modules/
mv src/records src/modules/
mv src/users src/modules/
Enter fullscreen mode Exit fullscreen mode

Also, we need to update AppModule

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './modules/database/database.module';
import { ModulesModule } from './modules/modules.module';

@Module({
  imports: [DatabaseModule, ModulesModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

ModulesModule will be as following:

import { Module } from '@nestjs/common';
import { AccountsModule } from './accounts/accounts.module';
import { CategoriesModule } from './categories/categories.module';
import { RecordsModule } from './records/records.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [AccountsModule, CategoriesModule, RecordsModule, UsersModule],
})
export class ModulesModule {}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add .
git commit -m "Accounts, Categories, Records and Users modules (initial) added"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update Accounts Module


Add enum AccountTypes

Create a file src/modules/accounts/entities/account.type.ts with the following content in it:

export enum AccountTypes {
  expense = 'expense',
  income = 'income',
  other = 'other',
}
Enter fullscreen mode Exit fullscreen mode

Lets organize all the records in this API by Expense and Income. This will allow us to calculate a general balance. As the following formula:

$$
balance = incomes - expenses
$$

Update AccountEntity

Add the following code to src/modules/accounts/account.entity.ts

import { Column, CreateDateColumn, Entity, Generated, PrimaryColumn, UpdateDateColumn } from 'typeorm';

import { AccountTypes } from './account.type';

@Entity('accounts')
export class AccountEntity {
  @PrimaryColumn()
  @Generated('uuid')
  id: string;

  @Column({ type: 'varchar', length: 255 })
  name: string;

  @Column({ type: 'varchar', length: 1024 })
  description: string;

  @Column({ type: 'enum', enum: AccountTypes })
  type: AccountTypes;

  @CreateDateColumn({
    type: 'timestamp',
    precision: 3,
    default: () => 'CURRENT_TIMESTAMP(3)',
  })
  createdAt: Date;

  @UpdateDateColumn({
    type: 'timestamp',
    precision: 3,
    default: () => 'CURRENT_TIMESTAMP(3)',
    onUpdate: 'CURRENT_TIMESTAMP(3)',
  })
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/accounts/
git commit -m "Update account entity"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update Categories Module


Add enum CategoryTypes

Create a file src/modules/categories/entities/category.type.ts with the following content in it:

export enum CategoryTypes {
  expense = 'expense',
  income = 'income',
}
Enter fullscreen mode Exit fullscreen mode

Update CategoryEntity

Add the following code to src/modules/categories/category.entity.ts

import { Column, CreateDateColumn, Entity, Generated, PrimaryColumn, UpdateDateColumn } from 'typeorm';

import { CategoryTypes } from './category.type';

@Entity('categories')
export class CategoryEntity {
  @PrimaryColumn()
  @Generated('uuid')
  id: string;

  @Column({ type: 'varchar', length: 255 })
  name: string;

  @Column({ type: 'varchar', length: 1024 })
  description: string;

  @Column({ type: 'enum', enum: CategoryTypes })
  type: CategoryTypes;

  @CreateDateColumn({
    type: 'timestamp',
    precision: 3,
    default: () => 'CURRENT_TIMESTAMP(3)',
  })
  createdAt: Date;

  @UpdateDateColumn({
    type: 'timestamp',
    precision: 3,
    default: () => 'CURRENT_TIMESTAMP(3)',
    onUpdate: 'CURRENT_TIMESTAMP(3)',
  })
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/categories/
git commit -m "Update category entity"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update Records Module


Add enum RecordTypes

Create a file src/modules/records/entities/record.type.ts with the following content in it:

export enum RecordTypes {
  expense = 'expense',
  income = 'income',
}
Enter fullscreen mode Exit fullscreen mode

Update RecordEntity

Add the following code to src/modules/records/record.entity.ts

import {
  Column,
  CreateDateColumn,
  Entity,
  Generated,
  ManyToOne,
  PrimaryColumn,
  RelationId,
  UpdateDateColumn,
} from 'typeorm';

import { AccountEntity } from 'src/modules/accounts/entities/account.entity';
import { CategoryEntity } from 'src/modules/categories/entities/category.entity';
import { RecordTypes } from './record.type';
import { UserEntity } from 'src/modules/users/entities/user.entity';

@Entity('records')
export class RecordEntity {
  @PrimaryColumn()
  @Generated('uuid')
  id: string;

  @Column('decimal', { name: 'amount', precision: 32, scale: 12 })
  amount: string;

  @Column({ type: 'varchar', length: 3 }) // Example: USD
  currencyCode: string;

  @Column({ type: 'date', nullable: true })
  date: Date;

  @Column({ type: 'varchar', length: 255 })
  name: string;

  @Column({ type: 'varchar', length: 1024 })
  description: string;

  @Column({ type: 'enum', enum: RecordTypes })
  type: RecordTypes;

  @ManyToOne(() => AccountEntity, (account) => account.records)
  account: AccountEntity;

  @RelationId((record: RecordEntity) => record.account)
  @Column({ type: 'uuid', nullable: true })
  accountId: string;

  @ManyToOne(() => CategoryEntity, (category) => category.records)
  category: CategoryEntity;

  @RelationId((record: RecordEntity) => record.category)
  @Column({ type: 'uuid', nullable: true })
  categoryId: string;

  @ManyToOne(() => UserEntity, (user) => user.records)
  user: UserEntity;

  @RelationId((record: RecordEntity) => record.user)
  @Column({ type: 'uuid', nullable: true })
  userId: string;

  @CreateDateColumn({
    type: 'timestamp',
    precision: 3,
    default: () => 'CURRENT_TIMESTAMP(3)',
  })
  createdAt: Date;

  @UpdateDateColumn({
    type: 'timestamp',
    precision: 3,
    default: () => 'CURRENT_TIMESTAMP(3)',
    onUpdate: 'CURRENT_TIMESTAMP(3)',
  })
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Update CategoryEntity

Update src/modules/categories/category.entity.ts with the following lines:

..
import { RecordEntity } from 'src/modules/records/entities/record.entity';
..

..
  @OneToMany(() => RecordEntity, (record) => record.category)
  records: RecordEntity[];
..
Enter fullscreen mode Exit fullscreen mode

Update AccountEntity

Update src/modules/accounts/account.entity.ts with the following lines:

..
import { RecordEntity } from 'src/modules/records/entities/record.entity';
..

..
  @OneToMany(() => RecordEntity, (record) => record.account)
  records: RecordEntity[];
..
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/accounts/ src/modules/categories src/modules/records
git commit -m "Update record entity"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update Users Module


Update UserEntity

Add the following code to src/modules/users/user.entity.ts

import { BeforeInsert, Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import * as bcrypt from 'bcrypt';

import { RecordEntity } from 'src/modules/records/entities/record.entity';

@Entity()
export class UserEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

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

  @Column()
  password: string;

  @Column({ nullable: true })
  refreshToken: string;

  @Column({ nullable: true })
  accessToken: string;

  @Column({ nullable: true })
  accessTokenExpires: Date;

  @Column({ nullable: true })
  oauthProvider: string;

  @Column({ nullable: true })
  oauthId: string;

  @BeforeInsert()
  async hashPassword() {
    this.password = await bcrypt.hash(this.password, 10);
  }

  @OneToMany(() => RecordEntity, (record) => record.user)
  records: RecordEntity[];
}
Enter fullscreen mode Exit fullscreen mode

Install bcrypt and @types/bcrypt

bun install bcrypt --save
bun install @types/bcrypt --save-dev
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/records/ src/modules/users/ package.json package-lock.json
git commit -m "Update user entity"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update entities on DatabaseModule


Add entities to DatabaseModule :

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

import { AccountEntity } from '../accounts/entities/account.entity';
import { CategoryEntity } from '../categories/entities/category.entity';
import { RecordEntity } from '../records/entities/record.entity';
import { UserEntity } from '../users/entities/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'nestjs_expenses_dev',
      entities: [AccountEntity, CategoryEntity, RecordEntity, UserEntity],
      synchronize: true,
    }),
  ],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/database/database.module.ts
git commit -m "Update database module with entities configured"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Test your changes are working as expected

bun start
Enter fullscreen mode Exit fullscreen mode
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [NestFactory] Starting Nest application...
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] DatabaseModule dependencies initialized +49ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] ModulesModule dependencies initialized +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] AccountsModule dependencies initialized +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] CategoriesModule dependencies initialized +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] RecordsModule dependencies initialized +1ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] UsersModule dependencies initialized +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +56ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RoutesResolver] AppController {/}: +9ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RoutesResolver] AccountsController {/accounts}: +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/accounts, POST} route +1ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/accounts, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/accounts/:id, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/accounts/:id, PATCH} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/accounts/:id, DELETE} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RoutesResolver] CategoriesController {/categories}: +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/categories, POST} route +1ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/categories, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/categories/:id, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/categories/:id, PATCH} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/categories/:id, DELETE} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RoutesResolver] RecordsController {/records}: +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/records, POST} route +1ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/records, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/records/:id, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/records/:id, PATCH} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/records/:id, DELETE} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RoutesResolver] UsersController {/users}: +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/users, POST} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/users, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/users/:id, GET} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/users/:id, PATCH} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [RouterExplorer] Mapped {/users/:id, DELETE} route +0ms
[Nest] 59339  - 07/29/2024, 12:23:40 AM     LOG [NestApplication] Nest application successfully started +1ms
Enter fullscreen mode Exit fullscreen mode

Install class-validator and class-transformer


To install the necessary packages for validation and transformation, run the following command:

bun install class-validator class-transformer --save
Enter fullscreen mode Exit fullscreen mode

These packages will help us validate incoming data and transform objects between plain JavaScript objects and class instances.

Configure ValuationPipe

Set up the ValiationPipe in your main.ts file to handle validation errors and customize the error response.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Update AccountsModule - CRUD actions


Let's update the CreateAccountDto to include the necessary fields for creating an account. Open the file src/modules/accounts/dto/create-account.dto.ts and add the following code:

import { IsString, IsNotEmpty, IsOptional, ValidateIf, IsUUID } from 'class-validator';
import { AccountTypes } from '../entities/account.type';

export class CreateAccountDto {
  @ValidateIf((o) => typeof o.id === 'string')
  @IsUUID()
  @IsOptional()
  id?: string;

  @ValidateIf((o) => typeof o.description === 'string')
  @IsString()
  @IsOptional()
  description?: string;

  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @IsNotEmpty()
  type: AccountTypes;
}
Enter fullscreen mode Exit fullscreen mode

This DTO will ensure that we receive the required data when creating a new account. Now let's move on to implementing the CRUD methods in the AccountController.

Implement the CRUD methods in AccountController

First, update the src/modules/accounts/accounts.controller.ts to handle the CRUD operations:

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { AccountsService } from './accounts.service';
import { CreateAccountDto } from './dto/create-account.dto';
import { UpdateAccountDto } from './dto/update-account.dto';

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

  @Post()
  create(@Body() createAccountDto: CreateAccountDto) {
    return this.accountsService.create(createAccountDto);
  }

  @Get()
  findAll() {
    return this.accountsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.accountsService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateAccountDto: UpdateAccountDto) {
    return this.accountsService.update(id, updateAccountDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.accountsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update AccountService

We need to implement the CRUD operations in the src/modules/accounts/accounts.service.ts to support the methods in the controller.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateAccountDto } from './dto/create-account.dto';
import { UpdateAccountDto } from './dto/update-account.dto';
import { AccountEntity } from './entities/account.entity';

@Injectable()
export class AccountsService {
  constructor(
    @InjectRepository(AccountEntity)
    private accountsRepository: Repository<AccountEntity>,
  ) {}

  create(createAccountDto: CreateAccountDto): Promise<AccountEntity> {
    const account = this.accountsRepository.create(createAccountDto);
    return this.accountsRepository.save(account);
  }

  findAll(): Promise<AccountEntity[]> {
    return this.accountsRepository.find();
  }

  findOne(id: string): Promise<AccountEntity> {
    return this.accountsRepository.findOne({ where: { id } });
  }

  async update(id: string, updateAccountDto: UpdateAccountDto): Promise<AccountEntity> {
    await this.accountsRepository.update(id, updateAccountDto);
    return this.accountsRepository.findOne({ where: { id } });
  }

  async remove(id: string): Promise<void> {
    await this.accountsRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update AccountsModule

We need to add TypeOrmModule.forFeature([AccountEntity]) to AcountsModule

Update the src/modules/accounts/accounts.module.ts file to include the TypeOrmModule for the AccountEntity:

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

import { AccountEntity } from './entities/account.entity';
import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';

@Module({
  imports: [TypeOrmModule.forFeature([AccountEntity])],
  controllers: [AccountsController],
  providers: [AccountsService],
  exports: [TypeOrmModule],
})
export class AccountsModule {}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/accounts/ src/main.ts package-lock.json package.json
git commit -m "Implement CRUD operations for AccountsModule"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update CategoriesModule - CRUD actions


Let's update the CreateCategoryDto to include the necessary fields for creating an account. Open the file src/modules/categories/dto/create-category.dto.ts and add the following code:

import { IsString, IsNotEmpty, IsOptional, ValidateIf, IsUUID } from 'class-validator';
import { AccountTypes } from '../entities/account.type';

export class CreateAccountDto {
  @ValidateIf((o) => typeof o.id === 'string')
  @IsUUID()
  @IsOptional()
  id?: string;

  @ValidateIf((o) => typeof o.description === 'string')
  @IsString()
  @IsOptional()
  description?: string;

  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @IsNotEmpty()
  type: AccountTypes;
}

Enter fullscreen mode Exit fullscreen mode

This DTO will ensure that we receive the required data when creating a new category.

Update CategoryController

This controller will handle endpoints for creating, reading, updating, and deleting categories. Below is the implementation for the CRUD operations in the category service:

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { CategoriesService } from './categories.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';

@Controller('categories')
export class CategoriesController {
  constructor(private readonly categoriesService: CategoriesService) {}

  @Post()
  create(@Body() createCategoryDto: CreateCategoryDto) {
    return this.categoriesService.create(createCategoryDto);
  }

  @Get()
  findAll() {
    return this.categoriesService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.categoriesService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateCategoryDto: UpdateCategoryDto) {
    return this.categoriesService.update(id, updateCategoryDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.categoriesService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update CategoryService

To implement the CRUD operations in the CategoryService, update the src/modules/categories/categories.service.ts file as follows:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { CategoryEntity } from './entities/category.entity';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';

@Injectable()
export class CategoriesService {
  constructor(
    @InjectRepository(CategoryEntity)
    private categoriesRepository: Repository<CategoryEntity>,
  ) {}

  create(createCategoryDto: CreateCategoryDto): Promise<CategoryEntity> {
    const category = this.categoriesRepository.create(createCategoryDto);
    return this.categoriesRepository.save(category);
  }

  findAll(): Promise<CategoryEntity[]> {
    return this.categoriesRepository.find();
  }

  findOne(id: string): Promise<CategoryEntity> {
    return this.categoriesRepository.findOne({ where: { id } });
  }

  async update(id: string, updateCategoryDto: UpdateCategoryDto): Promise<CategoryEntity> {
    await this.categoriesRepository.update(id, updateCategoryDto);
    return this.categoriesRepository.findOne({ where: { id } });
  }

  async remove(id: string): Promise<void> {
    await this.categoriesRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update CategoriesModule

We need to add TypeOrmModule.forFeature([CategoryEntity]) to CategoriesModule

Update the src/modules/categories/categories.module.ts file to include the TypeOrmModule for the CategoryEntity:

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

import { CategoriesController } from './categories.controller';
import { CategoriesService } from './categories.service';
import { CategoryEntity } from './entities/category.entity';

@Module({
  imports: [TypeOrmModule.forFeature([CategoryEntity])],
  controllers: [CategoriesController],
  providers: [CategoriesService],
  exports: [TypeOrmModule],
})
export class CategoriesModule {}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/categories/
git commit -m "Implement CRUD operations for CategoriesModule"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update RecordsModule - CRUD actions


Update CreateRecordDto

Below is the DTO for creating a new record:

import { IsString, IsNotEmpty, IsDate, IsUUID, ValidateIf, IsOptional } from 'class-validator';

export class CreateRecordDto {
  @ValidateIf((o) => typeof o.id === 'string')
  @IsUUID()
  @IsOptional()
  readonly id?: string;

  @IsString()
  @IsNotEmpty()
  readonly amount: string;

  @IsString()
  @IsNotEmpty()
  readonly currencyCode: string;

  @IsString()
  @IsNotEmpty()
  readonly name: string;

  @ValidateIf((o) => typeof o.description === 'string')
  @IsString()
  @IsOptional()
  readonly description?: string;

  @IsDate()
  @IsNotEmpty()
  readonly date: Date;

  @ValidateIf((o) => typeof o.accountId === 'string')
  @IsUUID()
  @IsOptional()
  readonly accountId?: string;

  @ValidateIf((o) => typeof o.categoryId === 'string')
  @IsUUID()
  @IsOptional()
  readonly categoryId?: string;

  @ValidateIf((o) => typeof o.userId === 'string')
  @IsUUID()
  @IsOptional()
  readonly userId?: string;
}

Enter fullscreen mode Exit fullscreen mode

Update RecordsController

This controller will handle the CRUD operations for records, allowing us to create, read, update, and delete record entries.

Below is the implementation for the CRUD operations in the record service:

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { RecordsService } from './records.service';
import { CreateRecordDto } from './dto/create-record.dto';
import { UpdateRecordDto } from './dto/update-record.dto';

@Controller('records')
export class RecordsController {
  constructor(private readonly recordsService: RecordsService) {}

  @Post()
  create(@Body() createRecordDto: CreateRecordDto) {
    return this.recordsService.create(createRecordDto);
  }

  @Get()
  findAll() {
    return this.recordsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.recordsService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateRecordDto: UpdateRecordDto) {
    return this.recordsService.update(id, updateRecordDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.recordsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update RecordService

To ensure that the provided categoryId and accountId exist, we need to validate them before proceeding with the CRUD operations. Here is how we can implement this in the RecordsService:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateRecordDto } from './dto/create-record.dto';
import { UpdateRecordDto } from './dto/update-record.dto';
import { RecordEntity } from './entities/record.entity';
import { CategoryEntity } from '../categories/entities/category.entity';
import { AccountEntity } from '../accounts/entities/account.entity';

@Injectable()
export class RecordsService {
  constructor(
    @InjectRepository(RecordEntity)
    private recordsRepository: Repository<RecordEntity>,
    @InjectRepository(CategoryEntity)
    private categoriesRepository: Repository<CategoryEntity>,
    @InjectRepository(AccountEntity)
    private accountsRepository: Repository<AccountEntity>,
  ) {}

  async validateCategoryAndAccount(categoryId: string, accountId: string): Promise<void> {
    const category = await this.categoriesRepository.findOne({ where: { id: categoryId } });
    if (!category) {
      throw new NotFoundException(`Category with ID ${categoryId} not found`);
    }

    const account = await this.accountsRepository.findOne({ where: { id: accountId } });
    if (!account) {
      throw new NotFoundException(`Account with ID ${accountId} not found`);
    }
  }

  async create(createRecordDto: CreateRecordDto): Promise<RecordEntity> {
    await this.validateCategoryAndAccount(createRecordDto.categoryId, createRecordDto.accountId);
    const record = this.recordsRepository.create(createRecordDto);
    return this.recordsRepository.save(record);
  }

  async update(id: string, updateRecordDto: UpdateRecordDto): Promise<RecordEntity> {
    await this.validateCategoryAndAccount(updateRecordDto.categoryId, updateRecordDto.accountId);
    await this.recordsRepository.update(id, updateRecordDto);
    return this.recordsRepository.findOne({ where: { id } });
  }

  findAll(): Promise<RecordEntity[]> {
    return this.recordsRepository.find();
  }

  findOne(id: string): Promise<RecordEntity> {
    return this.recordsRepository.findOne({ where: { id } });
  }

  async remove(id: string): Promise<void> {
    await this.recordsRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update RecordsModule

We need to add TypeOrmModule.forFeature([RecordEntity]) , AccountsModule and CategoriesModule to import on RecordsModule

Update the src/modules/records/records.module.ts file to include the TypeOrmModule for the RecordEntity:

import { Module } from '@nestjs/common';
import { RecordsService } from './records.service';
import { RecordsController } from './records.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RecordEntity } from './entities/record.entity';
import { CategoriesModule } from '../categories/categories.module';
import { AccountsModule } from '../accounts/accounts.module';

@Module({
  imports: [TypeOrmModule.forFeature([RecordEntity]), AccountsModule, CategoriesModule],
  controllers: [RecordsController],
  providers: [RecordsService],
  exports: [TypeOrmModule],
})
export class RecordsModule {}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/records/
git commit -m "Implement CRUD operations for RecordsModule"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Update UsersModule - CRUD actions


Update CreateUserDto

Below is the DTO for creating a new user:

import { IsString, IsNotEmpty, IsEmail, IsOptional, IsUUID, ValidateIf } from 'class-validator';

export class CreateUserDto {
  @ValidateIf((o) => typeof o.id === 'string')
  @IsUUID()
  @IsOptional()
  readonly id?: string;

  @IsEmail()
  @IsNotEmpty()
  readonly email: string;

  @IsString()
  @IsNotEmpty()
  readonly password: string;
}
Enter fullscreen mode Exit fullscreen mode

Update UserController

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

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

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update UserService

The UsersService will handle the business logic for user management. Below is the implementation for the CRUD operations in the UsersService:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(UserEntity)
    private usersRepository: Repository<UserEntity>,
  ) {}

  create(createUserDto: CreateUserDto): Promise<UserEntity> {
    const user = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(user);
  }

  findAll(): Promise<UserEntity[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<UserEntity> {
    return this.usersRepository.findOne({ where: { id } });
  }

  async update(id: string, updateUserDto: UpdateUserDto): Promise<UserEntity> {
    await this.usersRepository.update(id, updateUserDto);
    return this.usersRepository.findOne({ where: { id } });
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update UsersModule

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [TypeOrmModule],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/modules/users/
git commit -m "Implement CRUD operations for UsersModule"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Add AuthorizationModule


In this section, we'll implement authentication and authorization for our API using JSON Web Tokens (JWT) and Passport.js. The AuthorizationModule will handle user registration, login, and token validation.

Install npm dependencies

bun install @nestjs/jwt @nestjs/passport passport-jwt
Enter fullscreen mode Exit fullscreen mode

Create module folder

mkdir src/modules/authorization
Enter fullscreen mode Exit fullscreen mode

Create AuthorizationModule

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { AuthorizationController } from './authorization.controller';
import { AuthorizationService } from './authorization.service';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '60m' },
    }),
  ],
  providers: [AuthorizationService, JwtStrategy],
  controllers: [AuthorizationController],
  exports: [AuthorizationService],
})
export class AuthorizationModule {}
Enter fullscreen mode Exit fullscreen mode

This module sets up the necessary dependencies for authentication, including the JwtModule and PassportModule. We'll implement the details in the following sections.

Add AuthorizationController

Create the AuthorizationController in the src/modules/authorization/authorization.controller.ts file:

import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthorizationService } from './authorization.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
export class AuthorizationController {
  constructor(private readonly authService: AuthorizationService) {}

  @Post('register')
  async register(@Body() createUserDto: CreateUserDto) {
    return this.authService.register(createUserDto);
  }

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

This controller defines two endpoints: one for user registration and another for user login. Next, let's implement the AuthorizationService to handle the business logic for these operations.

Add AuthorizationService

Create the AuthorizationService in thesrc/modules/authorization/authorization.service.tsfile:

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { LoginDto } from './dto/login.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthorizationService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async register(createUserDto: CreateUserDto) {
        const user = await this.usersService.create(createUserDto);
    return this.generateToken(user);
  }

  async login(loginDto: LoginDto) {
    const user = await this.usersService.findByEmail(loginDto.email);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    const isPasswordValid = await bcrypt.compare(loginDto.password, user.password);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return this.generateToken(user);
  }

  private generateToken(user: any) {
    const payload = { email: user.email, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

This service handles user registration and login, using bcrypt for password hashing and JWT for token generation. Next, we'll configure the JwtModule and PassportModule.

Update UsersService

We need to update the UsersService to include a method for finding a user by email. This is necessary for the login functionality in the AuthorizationService. Add the following method to the UsersService class:

  findByEmail(email: string): Promise<UserEntity> {
    return this.usersRepository.findOne({ where: { email } });
  }
Enter fullscreen mode Exit fullscreen mode

This method will allow us to look up a user by their email address during the login process.

Create LoginDto

Create the LoginDto in thesrc/modules/authorization/dto/login.dto.tsfile:

import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class LoginDto {
  @IsEmail()
  @IsNotEmpty()
  readonly email: string;

  @IsString()
  @IsNotEmpty()
  readonly password: string;
}
Enter fullscreen mode Exit fullscreen mode

This DTO will be used to validate the login credentials sent by the user when attempting to log in.

Create JwtStrategy file

Create the JwtStrategy in thesrc/modules/authorization/jwt.strategy.tsfile:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}
Enter fullscreen mode Exit fullscreen mode

This strategy will be used to validate JWT tokens in incoming requests. It extracts the token from the Authorization header and verifies it using the secret key.

Commit your changes

git add src/modules/authorization/ src/modules/users/ src/modules/modules.module.ts package.json package-lock.json
git commit -m "Add AuthorizationModule for login and register users"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Add OpenAPI with Swagger

bun install --save @nestjs/swagger
Enter fullscreen mode Exit fullscreen mode

Update main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  const swaggerConfig = new DocumentBuilder()
    .setTitle('Expenses Manager')
    .setDescription('The Open API for Expenses manager')
    .setVersion('1.0')
    .setExternalDoc('Swagger.json', '/api/swagger-json')
    .addTag('expenses')
    .build();
  const document = SwaggerModule.createDocument(app, swaggerConfig, {
    deepScanRoutes: true,
    operationIdFactory: (_: string, methodKey: string) => methodKey,
  });
  SwaggerModule.setup('api/swagger', app, document);

  await app.listen(3000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

Commit your changes

git add src/main.ts package.json
git commit -m "Add OpenApi with Swagger"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Create UI (User Interface)

  • Option 1: Bot Telegram
  • Option 2: Single Page Application with Angular
  • Option 3: Single Page Application with React

Top comments (0)