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

Introduction

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

The idea of this project is to create a User Interface build on Angular or React.

This a personal project to manage my …


This content originally appeared on DEV Community and was authored by Ramon Serrano

Introduction

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

The idea of this project is to create a User Interface build on Angular or React.

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

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

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"

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

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

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

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 {}

ℹ️ 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 {}

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)...

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

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

docker exec -it mysql mysql -uroot -p

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>

Create the database, for example:

CREATE DATABASE nestjs_expenses_dev;

Then exit from mysql CLI:

exit

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 {}

Test now your connection running:

bun start

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

Commit your changes

git add .
git commit -m "DatabaseModule added"
git push origin main

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

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.

Categories Module

npx nest generate resource categories

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)

Records Module

npx nest generate resource records

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)

Users Module

npx nest generate resource users

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)

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/

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 {}

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 {}

Commit your changes

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

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',
}

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;
}

Commit your changes

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

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',
}

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;
}

Commit your changes

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

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',
}

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;
}

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[];
..

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[];
..

Commit your changes

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

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[];
}

Install bcrypt and @types/bcrypt

bun install bcrypt --save
bun install @types/bcrypt --save-dev

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

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 {}

Commit your changes

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

Test your changes are working as expected

bun start
[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

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

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();

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;
}

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);
  }
}

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);
  }
}

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 {}

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

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;
}

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);
  }
}

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);
  }
}

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 {}

Commit your changes

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

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;
}

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);
  }
}

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);
  }
}

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 {}

Commit your changes

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

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;
}

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);
  }
}

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);
  }
}

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 {}

Commit your changes

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

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

Create module folder

mkdir src/modules/authorization

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 {}

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);
  }
}

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),
    };
  }
}

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 } });
  }

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;
}

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 };
  }
}

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

Add OpenAPI with Swagger

bun install --save @nestjs/swagger

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();

Commit your changes

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

Create UI (User Interface)

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


This content originally appeared on DEV Community and was authored by Ramon Serrano


Print Share Comment Cite Upload Translate Updates
APA

Ramon Serrano | Sciencx (2024-08-18T17:58:41+00:00) Part 1: Create an API with Nest.js [2024]. Retrieved from https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/

MLA
" » Part 1: Create an API with Nest.js [2024]." Ramon Serrano | Sciencx - Sunday August 18, 2024, https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/
HARVARD
Ramon Serrano | Sciencx Sunday August 18, 2024 » Part 1: Create an API with Nest.js [2024]., viewed ,<https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/>
VANCOUVER
Ramon Serrano | Sciencx - » Part 1: Create an API with Nest.js [2024]. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/
CHICAGO
" » Part 1: Create an API with Nest.js [2024]." Ramon Serrano | Sciencx - Accessed . https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/
IEEE
" » Part 1: Create an API with Nest.js [2024]." Ramon Serrano | Sciencx [Online]. Available: https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/. [Accessed: ]
rf:citation
» Part 1: Create an API with Nest.js [2024] | Ramon Serrano | Sciencx | https://www.scien.cx/2024/08/18/part-1-create-an-api-with-nest-js-2024/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.