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
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
, when trying to push my changes on my repository. So I suggest to take the following notes in consideration:
and the repository exists'
- Configure your SSH keys on your remote git tool (GitHub, GitLab, Bitbucket)
- Run
eval "$(ssh-agent -s)"
, afterssh-add
and then try againgit 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.ts
file:
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.ts
file:
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.ts
file:
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
Top comments (0)