B. TypeORM Basic CRUD
TypeORM merupakan ORM (Object Relational Mapping yang dapat berjalan pada NodeJS). Selain TypeOrm , ORM lain yang bisa digunakan pada nestjs adalah sequelize
, prisma
, knex
dan lain-lain.
untuk memulai menggukan TypeORM pada nestjs kita instalasi terlebih dahulu
Kemudian kita akan membuat file baru untuk menyimpan konfigurasi TypeOrm di aplikasi kita
0. Instalasi MYSQL dan PhpMyAdmin
Pada latihan ini kita akan lakukan instalasi mysql dan phpMyAdmin menggunakan docker. buatlah file dengan nama docker-compose.yml
version: "3.9"
services:
db:
image: mysql:8.0
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_PASSWORD: root
TZ: Asia/Jakarta
ports:
- 3308:3306
networks:
- internal_network
volumes:
- ./app/:/user
phpmyadmin:
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
links:
- db
environment:
PMA_HOST: db
MYSQL_ROOT_PASSWORD: root
restart: always
ports:
- 8081:80
networks:
- internal_network
networks:
internal_network:
external: true
Kemudian kita akan buat network pada docker terlebih dahulu
Kemudian kita instalasi mysql dan phpMyAdmin pada docker mengggunakan docker compose
Tunggu sampai selesai proses instalasi
2. Global Configuration
ita akan membahas dotenv, dimana file ini digunakan untuk memyimpan konfigurasi pada aplikasi kita. Kalau sebelum nya konfigurasi kita tulis secara hardcode pada koding seperti pada saat membuat konfig typeorm
Pertama kita instalasi dulu package untuk config
2. Import Module Config pada app module
kita import pada app module sebagai global agar bisa diakses oleh semua module
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // konfigurasi is global untuk semua module
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
3. Buat File .env
buatlah file .env
DB_HOST = localhost // alamat server mysql
DB_USERNAME = root // username dari mysql
DB_PASSWORD = root // password dari mysq;
DB_DATABASE = belajar_nest_js // nama database
DB_PORT = 3308 // port dari mysql
1. TypeOrm Config
Buatlah folder config pada folder src , kemudian buatlah file typeorm.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Auth } from '../auth-service.entity';
export const typeOrmConfig: TypeOrmModuleOptions = {
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: [Auth],
synchronize: true,
logging: true,
};
2. Import TypeOrmConfig pada app module
import module TypeOrm pada app.module.ts agar typeorm bisa digunakan pada aplikasi kita.
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { LatihanModule } from "./latihan/latihan.module";
import { BookModule } from "./book/book.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { typeOrmConfig } from "./config/typeorm.config";
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
useFactory: async () => {
const { typeOrmConfig } = await import('./config/typeorm.config');
return typeOrmConfig;
},
}),, LatihanModule, BookModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
3. Membuat Book Entity
Kemudian kita akan membuat entity untuk membuat table pada database mysql.
Buatlah file dengan nama book.entity.ts
pada folder book, seperti berikut
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class Book extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
author: string;
@Column()
year: number;
@Column({ type: "datetime", default: () => "CURRENT_TIMESTAMP" })
created_at: Date;
@Column({ type: "datetime", default: () => "CURRENT_TIMESTAMP" })
updated_at: Date;
}
Kemudian kita import entity pada book.module.ts
import { Module } from "@nestjs/common";
import { BookService } from "./book.service";
import { BookController } from "./book.controller";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Book } from "./book.entity"; //import dari book.entity.ts
@Module({
imports: [TypeOrmModule.forFeature([Book])], // import dengan TypeOrm For Feature
providers: [BookService],
controllers: [BookController],
})
export class BookModule {}
Kemudian kita lihat apakah tabel sudah terbuat atau tidak pada database
Pada gambar tersebut terlihat kalau tabel sudah terbuat secara otomatis pada database. Jika kita mengalami kendala tabel tidak terbuat, silahkan cek kembali langkah-langkah di atas.
4. Menggunakan Book Entity pada BookService
Ketika kita sudah menggukana forFeature() pada module, selanjutnya kita akan menginject BookRepository ke dalam BookService dengan menggunakan @InjectRepository() decorator
import { Injectable, NotFoundException } from '@nestjs/common';
import { ResponseSuccess } from 'src/interface/response';
import { CreateBookDto, UpdateBookDto } from './book.dto';
import { InjectRepository } from '@nestjs/typeorm'; // import injectReposity
import { Book } from './book.entity'; // import Book Entiy
import { Repository } from 'typeorm'; import //import repository
@Injectable()
export class BookService {
//inject book repository ke service
constructor(
@InjectRepository(Book) private readonly bookRepository: Repository<Book>,
) {}
//inject book repository ke service
private books: {
id?: number;
title: string;
author: string;
year: number;
}[] = [
{
id: 1,
title: 'HTML CSS',
author: 'ihsanabuhanifah',
year: 2023,
},
];
getAllBooks(): ResponseSuccess {
return {
status: 'Success',
message: 'List Buku ditermukan',
data: this.books,
};
}
}
5. Membuat DTO pada pada fitur book
import { OmitType } from '@nestjs/mapped-types';
import { Type } from 'class-transformer';
import {
IsArray,
IsInt,
IsNotEmpty,
IsOptional,
Length,
ValidateNested,
Min,
Max,
} from 'class-validator';
import { PageRequestDto } from 'src/utils/dto/page.dto';
export class BookDto {
id: number;
@IsNotEmpty()
title: string;
@IsNotEmpty()
author: string;
@IsInt()
@Min(2020)
@Max(2023)
year: number;
}
export class CreateBookDto extends OmitType(BookDto, ['id']) {}
export class UpdateBookDto extends OmitType(BookDto, ['id']) {}
export class createBookArrayDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateBookDto)
data: CreateBookDto[];
}
6. Menambahkan Book ke Tabel dengan TypeOrm
Pada materi ini , kita akan berlatih menambahkan data ke tabel book pada database mengguankan method save()
dari TypeOrm
Perhatikan method createBook pada bookService, sebelumnya kita menggunakan database semetera pada array book seperti koding di bawah
createBook(createBookDto: CreateBookDto): ResponseSuccess {
const { title, author, year } = createBookDto;
this.books.push({
id: new Date().getTime(),
title: title,
author: author,
year: year,
});
return {
status: 'Success',
message: 'Berhasil menambakan buku',
};
}
selanjutnya kita kan ubah tempat menyimpan data ke dalam database dan menampilkannya , maka seperti koding di bawah ini
import {
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
...
async createBook(createBookDto: CreateBookDto): Promise<ResponseSuccess> {
const { title, author, year } = createBookDto;
try {
await this.bookRepository.save({
title: title,
author: author,
year: year,
});
return {
status: 'Success',
message: 'Berhasil menambakan buku',
};
} catch (err) {
throw new HttpException('Ada Kesalahan', HttpStatus.BAD_REQUEST);
}
}
...
Pada kode diatas, kita ubah method createBook
menjadi asynchronous
karena saat proses penyimpanan data akan ada jeda waktu menunggu sampai ada response, ntuk menjadikan asyncronous
kita cukup memberika keyword async.
Selanjutnya kita bikin kondisi jika penyimpanan gagal maka akan menampilkan pesan kesalahan "Ada Kesalahan" dengan kode 400.
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto} from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Post('/create')
createBook(@Body() payload: CreateBookDto) {
return this.bookService.createBook(payload);
}
}
Pengujian pada create
Kita cek apakah data berhasil masuk atau belum ke database
6. Menampilkan seluruh data Book dengan TypeOrm
kita akan menampilkan data dengan method find() pada typeOrm.
async getAllBooks(): Promise<ResponseSuccess> {
const result = await this.bookRepository.find();
return {
status: 'Success',
message: 'List Buku ditermukan',
data: result,
};
}
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto } from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Get('/list')
findAllBook() {
return this.bookService.getAllBooks();
}
}
Pengujian pada Postman
7. Menampilkan detail book dengan TypeOrm
...
async getDetail(id: number): Promise<ResponseSuccess> {
const detailBook = await this.bookRepository.findOne({
where: {
id,
},
});
if (detailBook === null) {
throw new NotFoundException(`Buku dengan id ${id} tidak ditemukan`);
}
return {
status: 'Success',
message: 'Detail Buku ditermukan',
data: detailBook,
};
}
...
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto} from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Get('detail/:id')
findOneBook(@Param('id') id: string) {
return this.bookService.getDetail(Number(id));
}
}
Pengujian pada Postman
8. Mengupdate book dengan TypeOrm
async updateBook(
id: number,
updateBookDto: UpdateBookDto,
): Promise<ResponseSuccess> {
const check = await this.bookRepository.findOne({
where: {
id,
},
});
if (!check)
throw new NotFoundException(`Buku dengan id ${id} tidak ditemukan`);
const update = await this.bookRepository.save({ ...updateBookDto, id: id });
return {
status: `Success `,
message: 'Buku berhasil di update',
data: update,
};
}
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto, UpdateBookDto} from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Put('update/:id')
updateBook(@Param('id') id: string, @Body() updateBookDto: UpdateBookDto) {
return this.bookService.updateBook(Number(id), updateBookDto);
}
}
Pengujian pada Update
Pengujian pada after update Setelah proses update berhasil, kita akan cek kembali menggunakan endpoint detail.
Pada gambar di atas, kita sudah berhasil merubah data sesuai yang di update
9. Menghapus book dengan TypeOrm
async deleteBook(id: number): Promise<ResponseSuccess> {
const check = await this.bookRepository.findOne({
where: {
id,
},
});
if (!check)
throw new NotFoundException(`Buku dengan id ${id} tidak ditemukan`);
await this.bookRepository.delete(id);
return {
status: `Success `,
message: 'Berhasil menghapus buku',
};
}
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto, UpdateBookDto} from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Delete('delete/:id')
deleteBook(@Param('id') id: string) {
return this.bookService.deleteBook(+id);
}
Pengujian pada Delete
10. Menambahkan Banyak Buku ke table dengan TypeOrm
...
async bulkCreate(payload: createBookArrayDto): Promise<ResponseSuccess> {
try {
let berhasil = 0;
let gagal = 0;
await Promise.all(
payload.data.map(async (data) => {
try {
await this.bookRepository.save(data);
berhasil += 1;
} catch {
gagal += 1;
}
}),
);
return this._success(`Berhasil menyimpan ${berhasil} dan gagal ${gagal}`);
} catch {
throw new HttpException('Ada Kesalahan', HttpStatus.BAD_REQUEST);
}
}
...
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto, UpdateBookDto, createBookArrayDto} from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Post('/create/bulk')
bulkCreateBook(@Body() payload: createBookArrayDto) {
return this.bookService.bulkCreate(payload);
}
Pengujian pada Postman
{
"data" : [
{
"title": "NestJS For Backend",
"author": "Ihsan",
"year": 2023
},
{
"title": "Become Network Engineer",
"author": "Fathi",
"year": 2021
},
{
"title": "HTML CSS",
"author": "Nur",
"year": 2022
},
{
"title": "TypeScript",
"author": "Ihsan",
"year": 2023
},
{
"title": "Server Admin",
"author": "Raihan",
"year": 2022
},
{
"title": "Database MySQL",
"author": "Akbar",
"year": 2023
},
{
"title": "React Developer",
"author": "Nur",
"year": 2023
},
{
"title": "NextJs Developer",
"author": "Ihsan",
"year": 2023
}
]
}
11. Membuat Pagination(Paging) pada menampilan semua data
Ketika membuat REST API untuk menampilkan semua data di tabel, maka kita perlu menggunakan paging agar query di backend tidak terlalu berat dan kita bisa membatasi berapa data yang ditampilkan dalam satu kali query.
Pada materi ini kita akan membahas bagaiaman membuat paging dengan typeorm dan nestjs
Langkah Pertama kita buat dulu response type untuk pagination
import { HttpStatus } from "@nestjs/common";
export interface ResponseSuccess {
statusCode?: HttpStatus;
status: string;
message: string;
data?: any;
}
export interface ResponsePagination extends ResponseSuccess {
pagination: {
total: number;
page: number;
pageSize: number;
};
}
Jadi ketika pagination kita wajibkan return memiliki object pagination denga property total data, page saat ini dan berapa data yang ditampilkan (pageSize)
Langkah Kedua kita buat dto untuk pagination , dengan membuat folder baru utils/dto
import { Type } from "class-transformer";
import { IsInt } from "class-validator";
export class PageRequestDto {
@IsInt()
@Type(() => Number)
page = 1;
@IsInt()
@Type(() => Number)
pageSize = 10;
}
Langkah Ketiga kita buat dto findBookDto dengan mengextends PageRequestDto
import { OmitType } from "@nestjs/mapped-types";
import { IsInt, IsNotEmpty, Min, Max, Length } from "class-validator";
import { PageRequestDto } from "src/utils/dto/page.dto";
export class BookDto {
id: number;
@IsNotEmpty()
@Length(4, 10)
title: string;
@IsNotEmpty()
author: string;
@IsInt()
@Min(2020)
@Max(2023)
year: number;
}
...
export class FindBookDto extends PageRequestDto {}
...
Langkah Ke Empat kita ubah koding findAllBook pada controller dan service
import {
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { ResponsePagination, ResponseSuccess } from 'src/interface/response';
import { CreateBookDto, FindBookDto, UpdateBookDto } from './book.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Book } from './book.entity';
import { Repository } from 'typeorm';
@Injectable()
export class BookService {
constructor(
@InjectRepository(Book) private readonly bookRepository: Repository<Book>,
) {}
async getAllBooks(query: FindBookDto): Promise<ResponsePagination> {
console.log('uqwey', query);
const { page, pageSize } = query;
const total = await this.bookRepository.count();
const result = await this.bookRepository.find({
skip: (Number(page) - 1) * Number(pageSize),
take: Number(pageSize),
});
return {
status: 'Success',
message: 'List Buku ditermukan',
data: result,
pagination: {
total: total,
page: Number(page),
pageSize: Number(pageSize),
},
};
}
...
}
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from "@nestjs/common";
import { BookService } from "./book.service";
import { CreateBookDto, FindBookDto, UpdateBookDto } from "./book.dto";
@Controller("book")
export class BookController {
constructor(private bookService: BookService) {}
@Get("/list")
findAllBook(@Query() findBookDto: FindBookDto) {
return this.bookService.getAllBooks(findBookDto);
}
}
Pengujian pada Postman
12. Implementasi Custom Decorator untuk meyederhanakan Paging
Selain mengguankan decorator bawaan seperti @Body()
, @Query()
, @Param()
pada controler, kita juga bisa membuat Custom Decorator seperti pada dokumentasi https://docs.nestjs.com/custom-decorators.
Pada contoh kasus kali ini, kita akan membuat Custom Decorator untuk paging sehingga, pada setiap service kita tidak perlu menghitung ulang page dan limit untuk paging
async getAllBooks(query: FindBookDto): Promise<ResponsePagination> {
...
const result = await this.bookRepository.find({
skip: (Number(page) - 1) * Number(pageSize),
take: Number(pageSize),
});
...
}
Perhatikan pada bagian skip
, disitu kita harus menghitung limit
.Bayangkan jika kita membuat fitur ini pada module lain, maka kita harus selalu menghitung limit. Tentu hal ini tidak efektif karena harus melalukan pekerjaan yang sama secara berulang. Pada materi ini kita akan membuat custom decorator agar kita tidak perlu menghitung ulang limit, namun nanti kita hanya tinggal menggunakan saja pada setiap service.
Pertama buatlah folder decorator pada folder utils
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Pagination = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
if (!!request.query.page === false) { //memberikan nilai default 1 jika tidak dikirim client
request.query.page = 1;
}
if (!!request.query.pageSize === false) { //memberikan nilai default 10 jika tidak dikirim client
request.query.pageSize = 10;
}
request.limit =
(Number(request.query.page) - 1) * Number(request.query.pageSize);
request.query.pageSize = Number(request.query.pageSize);
request.query.page = Number(request.query.page);
return request.query;
},
);
Pada kode di atas , kita menghitung limit dan kita return hasilnya pada decorator pagination
Selanjutnya kita perbaharui kode di main.ts larena kita menggunakan custom decorator maka kita harus mengaktifkan validateCustomDecorators
menjadi true
seperti pada kode di bawah.
main.ts
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,
forbidUnknownValues: true,
transform: true,
validateCustomDecorators: true,
transformOptions: {
enableImplicitConversion: true,
},
})
);
await app.listen(5002);
}
bootstrap();
Selanjutnya kita ganti decorator @Query()
pada findAllBook di book.controller.ts dengan custom decorator Pagination()
yang kita buat.
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { BookService } from './book.service';
import { CreateBookDto, FindBookDto, UpdateBookDto } from './book.dto';
import { Pagination } from 'src/utils/decorator/pagination.decorator';
@Controller('book')
export class BookController {
constructor(private bookService: BookService) {}
@Get('/list')
findAllBook(@Pagination() findBookDto: FindBookDto) {
return this.bookService.getAllBooks(findBookDto);
}
...
}
Implementasikan limit
dan pageSize
pada book.service.ts
async getAllBooks(query: FindBookDto): Promise<ResponsePagination> {
const { page, pageSize, limit } = query;
const total = await this.bookRepository.count();
const result = await this.bookRepository.find({
skip: limit,
take: pageSize,
});
return {
status: 'Success',
message: 'List Buku ditermukan',
data: result,
pagination: {
total: total,
page: page,
pageSize: pageSize,
},
};
}
saat kita nenambahkan limit
pada query maka akan muncul error, untuk mengatasi hal tersebut kita perbaharui page.dto.ts
import { Type } from "class-transformer";
import { IsInt, IsOptional } from "class-validator";
export class PageRequestDto {
@IsInt()
@Type(() => Number)
page = 1;
@IsInt()
@Type(() => Number)
pageSize = 10;
@IsInt()
@IsOptional()
limit;
}
Pengujian pada Postman
13. Membuat Filter page getAllBook
Seringkali ketika menampilkan data , kita membutuhkan filter fitur yang bisa dikombinasikan atau salah satu saja. Contoh pada kasus book kita kita bisa memcari berdasarkan salah satu dari author, title, year atau kombinasi ketiga nya.
Pada materi kali ini kita akan membuar fitur filter berdasrkan title, author, dan range tahun terbit seperti terlihat pada postman
Kita tambahkan option title, author, from_year, to_year pada book.dto.ts
import { OmitType } from "@nestjs/mapped-types";
import { Type } from "class-transformer";
import {
IsInt,
IsNotEmpty,
Min,
Max,
Length,
IsOptional,
} from "class-validator";
import { PageRequestDto } from "src/utils/dto/page.dto";
export class BookDto {
id: number;
@IsNotEmpty()
@Length(4, 10)
title: string;
@IsNotEmpty()
author: string;
@IsInt()
@Min(2020)
@Max(2023)
year: number;
}
export class CreateBookDto extends OmitType(BookDto, ["id"]) {}
export class UpdateBookDto extends OmitType(BookDto, ["id"]) {}
export class FindBookDto extends PageRequestDto {
@IsOptional()
title: string;
@IsOptional()
author: string;
@IsOptional()
@IsInt()
@Type(() => Number)
from_year: number;
@IsOptional()
@IsInt()
@Type(() => Number)
to_year: number;
}
Selanjutkan kita tambahkan condition statement pada method find tyoeorm
async getAllBooks(query: FindBookDto): Promise<ResponsePagination> {
const { page, pageSize, limit, title, author, from_year, to_year } = query;
console.log('q', query);
const total = await this.bookRepository.count();
const filter: {
[key: string]: any;
} = {};
if (title) {
filter.title = Like(`%${title}%`);
}
if (author) {
filter.author = Like(`%${author}%`);
}
if (from_year && to_year) {
filter.year = Between(from_year, to_year);
}
if (from_year && !!to_year === false) {
filter.year = Between(from_year, from_year);
}
const result = await this.bookRepository.find({
where: filter,
skip: limit,
take: pageSize,
});
return {
status: 'Success',
message: 'List Buku ditermukan',
data: result,
pagination: {
total: total,
page: page,
pageSize: pageSize,
},
};
}
Pengujian Postman