I. Ý tưởng

Về ý tưởng, Chúng ta sẽ xây dựng 1 Web application quản lý bài viết bao gồm:
- Frontend dùng ReactJS
- Backend dùng NestJS
- Database chúng ta sẽ sử dụng PostgreSQL
-̀ Việc xác thực chúng ta sẽ xác thực thông qua Auth0: đây là một dịch vụ quản lý xác thực và ủy quyền người dùng mà chúng ta không cần phải tốn công xây dựng hệ thống phức tạp trên server của mình nữa.

Và series này sẽ gồm 3 phần:
- Phần 1: Phát triển API Backend: cài đặt NestJS 9, PostgreSQL 12, Cấu hình sử dụng repository với Typeorm 0.3.x để tạo API CRUD Articles.
- Phần 2: Hướng dẫn cấu hình và cài đặt Auth0 để xác thưc NestJS API.
- Phần 3: Phát triển Web Application: cài đặt ReactJS, tạo giao diện, integration với API NestJS và xác thực, quản lý người dùng với Auth0.

Và sau đây là phần 1: cài đặt NestJS 9, PostgreSQL 12, Cấu hình sử dụng repository với Typeorm 0.3.x để tạo API CRUD Articles.

Phần 1 của series sẽ tập trung vào việc xây dựng phần Backend của ứng dụng. Chúng ta sẽ sử dụng NestJS, một framework Node.js hiệu suất cao và dễ sử dụng để xây dựng các API. Đồng thời, chúng ta cũng sẽ sử dụng PostgreSQL là cơ sở dữ liệu và TypeORM làm ORM (Object-Relational Mapping) để tương tác với cơ sở dữ liệu.

Trong phần này, chúng ta sẽ tạo một CRUD (Create, Read, Update, Delete) cho bài viết (Articles). Bằng cách sử dụng NestJS 9 và PostgreSQL 12, TypeORM 0.3.x .

Qua việc hoàn thành phần này, bạn sẽ có một nền tảng mạnh mẽ để xây dựng các chức năng phức tạp hơn trong ứng dụng của bạn. Hãy chuẩn bị tinh thần và bắt đầu khám phá những kiến thức thú vị và bài viết hữu ích trong loạt bài viết này.

Mọi người hãy đồng hành cùng mình trong hành trình phát triển ứng dụng đầy thú vị này nhé!

II. Xây dựng Backend APIs với Nest.js 9, PostgreSQL

1. Cài đặt NestJS

Sau đây mình sẽ hướng dẫn các bạn từng bước để phát triển API với NestJS.

  1. Cài nestjs cli
npm install -g @nestjs/cli

1C26P5vkIH4I_5CDEwp8dBht3nFtcsQme

  1. Tạo project với tên nestjs-auth0
nest new nestjs-auth0

Lưu ý: khi chạy lệnh ở bước này, nest cli sẽ cho chúng ta 3 lựa chọn, tùy dự án mà chúng ta lựa chọn option phù hợp, ở đây mình chọn cài đặt mặc định là npm

1tSwInghfOBhIZdCCcRydM5U6WP7oatHK

  1. Vào dự án và cài đặt package cần thiết:
# di chuyển vào dự án, giờ chúng ta sẽ thao tác tất cả các lệnh tại đây
cd nestjs-auth0

# package hỗ trợ file config, biến môi trường
npm install --save @nestjs/config dotenv

# package hỗ trợ database postgres
npm install --save @nestjs/typeorm typeorm pg

Vậy là đã xong phần cài đặt cơ bản, bây giờ các bạn thử start lên và kiểm tra xem ứng dụng đã chạy chưa nào.

npm run start:dev

Thao tác này sẽ chạy ứng dụng trên cổng 3000 trên máy cục bộ của bạn. Điều hướng đến http://localhost:3000 từ trình duyệt bạn chọn và bạn sẽ thấy ứng dụng của mình đang chạy. (mình sẽ hướng dẫn các bạn đổi port ở các step tiếp theo)

18fvddQy_SopqUYYS6BSu2kkMWP5gM6Y1

Sau khi xong phần cài đặt, thì các bạn dùng editor mở dự án của mình lên và bắt đầu những bước tiếp theo thôi.

2. Cấu hình database PostgreSQL 12

Trước khi cấu hình database trong source code thì chúng ta chuẩn bị trước biến môi trường cũng như môi trường phát triển cho database.

  1. tạo file .env ở thư mục dự án với nội dung sau
#app port, dùng để thay đổi Application PORT, có thể thay đổi trong file src/main.ts
APP_PORT=3010

#database
DATABASE_CONNECT=postgres
DATABASE_PORT=9432
DATABASE_USER=debug
DATABASE_PASSWORD=debug
DATABASE_NAME=debug
  1. Cài đặt môi trường database
  • Ở đây mình sử dụng docker để cấu hình và cài đặt postgres và trình quản lý database là adminer.

  • Tạo file nestjs-auth0/docker-compose.yml có nội dung như sau

version: '3.3'
services:
  adminer:
    image: adminer:latest
    ports:
      - 9081:8080
    networks:
      - auth0nestjs
  postgres:
    image: postgres:12
    volumes:
      - data-volume:/data/db
    ports:
      - ${DATABASE_PORT}:5432
    environment:
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_USER: ${DATABASE_USER}
      POSTGRES_DB: ${DATABASE_NAME}
    networks:
      - auth0nestjs
networks:
  auth0nestjs:
volumes:
  data-volume:
  • Start docker file
docker-compose up -d

1qRqudLnV4gwYsV5yxdb0-bA0RYCiGeQN

  • Sau khi đã start, các bạn vào adminer để kiểm tra kết nối

1nwtLUs0acKuzcOtaKB6-a-Dp3S44ASOU

1unWxY1AFWyFJEn-GxOVC3tuE80Kxg9JH

Xong phần cài đặt env, buớc tiếp theo chúng ta sẽ  cài đặt các file config để sử dụng postgreSQL trong NestJS:

1.Tạo folder src/config đây sẽ là nơi chứa tất cả config trong dự án của chúng ta.

  1. Tạo file src/config/env.config.ts : chứa các biến config
import * as dotenv from 'dotenv';
dotenv.config();

export const env = {
    APP_PORT: process.env.APP_PORT,
    APP_ENV: process.env.APP_ENV,
    DATABASE: {
        CONNECT: process.env.DATABASE_CONNECT as any,
        HOST: process.env.DATABASE_HOST,
        PORT: Number(process.env.DATABASE_PORT),
        USER: process.env.DATABASE_USER,
        PASSWORD: process.env.DATABASE_PASSWORD,
        NAME: process.env.DATABASE_NAME
    },
};
  1. Tạo file src/config/data-source.config.ts: cấu hình connect database của chúng ta.
import { DataSource, DataSourceOptions } from "typeorm";
import { env } from './env.config';

export const dataSourceOptions: DataSourceOptions = {
  type: env.DATABASE.CONNECT,
  host: env.DATABASE.HOST,
  port: env.DATABASE.PORT,
  username: env.DATABASE.USER,
  password: env.DATABASE.PASSWORD,
  database: env.DATABASE.NAME,
  entities: [__dirname + '/../**/*.entity.{js,ts}'],
  migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
  extra: {
    charset: 'utf8mb4_unicode_ci',
  },
  synchronize: false,
  logging: true,
};

const dataSource = new DataSource(dataSourceOptions);
export default dataSource;
  1. Tạo file src/config/database.config.ts: khai báo config, chúng ta sẽ import file này trong module app.module.ts.
import { TypeOrmModule } from '@nestjs/typeorm';
import { dataSourceOptions } from './data-source.config';

export const databaseConfig = TypeOrmModule.forRoot(dataSourceOptions);
  1. Sau khi xong phần config, các bạn mở file package.json lên và update nội dung, thêm phần sau vào phần "scripts" để có thể chạy migrations.
{
  ...
  "scripts": {
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",

    --- PHẦN ĐƯỢC THÊM (XÓA DÒNG NÀY) ---

    "typeorm:cli": "ts-node ./node_modules/typeorm/cli.js",
    "migration:create": "npm run typeorm:cli -- migration:create",
    "migration:run": "npm run typeorm:cli -- migration:run -d src/config/data-source.config.ts",
    "migration:revert": "npm run typeorm:cli -- migration:revert -d src/config/data-source.config.ts"

    --- PHẦN ĐƯỢC THÊM (XÓA DÒNG NÀY) ---

  },
  ...
}

  1. Tạo file migrate create-article để tạo bảng với tên articles, đường dẫn được lưu tại src/database/migrations
npm run migration:create src/database/migrations/create-article

1JuZt-B2NHkcTOeC5oLsOqAd0mfuD_hlN

  1. Sau khi tạo file xong, các bạn mở file migration vừa tạo lên và update lại nội dung như sau để tạo bảng articles
import { MigrationInterface, QueryRunner } from "typeorm"

export class CreateArticle1685464573824 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`
            CREATE TABLE "articles" (
                "id" SERIAL NOT NULL,
                "title" character varying NOT NULL, 
                "author" character varying NOT NULL, 
                "content" TEXT NOT NULL,
                "publication_date" DATE,
                "updated_at" timestamp,
                "created_at" timestamp,
                CONSTRAINT "PK_40808690eb7b915046558c0f81b" PRIMARY KEY ("id")
            )
        `);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP TABLE "articles"`);
    }
}
  1. Chạy lệnh run migration
npm run migration:run

1Zs5oRpodq_BVyYaWIfbreTunoANQfcN9

  1. Sau khi chạy xong, các bạn kiểm tra lại adminer đã tạo thành công được bảng articles. Và chúc mừng, bạn đã thành công config connect được database.
    1XDHEl4SWjkUhz5BLo9Qn-vsbkejmjloB

3. Tạo Module, Entity, Repository cho `articles`

Để tạo Module trong NestJS, chúng ta sử dụng nest cli

nest g mo article

Sau khi thực hiện lệnh này, NestJS sẽ tạo cho chúng ta 1 file src/article/article.module.ts nằm trong thư mục article (gọi là module article) và đồng thời tự động import ArticleModule vào file app.module.ts
1ruAg0kUhA7Asbuwg4abF6COU07eRx6h_

Tiếp theo, chúng ta mở file src/app.module.ts để cập nhật config database vào

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ArticleModule } from './article/article.module';
import { ConfigModule } from '@nestjs/config';
import { databaseConfig } from './config/database.config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    databaseConfig,
    ArticleModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

NestJS giao tiếp với database thông qua Typeorm, và để dễ dàng hơn cho việc truy vấn cũng như thao tác với SQL, chúng ta tạo ra 1 file `ArticleEntity`, chúng đóng vai trò là `Model` trong mô hình MVC quen thuộc.

Trong module article tạo file src/article/article.entity.ts với nội dung sau:

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

@Entity('articles')
export class ArticleEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  author: string;

  @Column({ name: 'publication_date' })
  publicationDate: Date;

  @Column('text')
  content: string;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}

Hiện tại, với bản NestJS version 9.x, thì Typeorm đang sử dụng là "0.3.x", thì xin thông báo tin buồn đối với các bạn có thói quen tạo file repository cho entity. Bởi vì từ version này, sẽ không còn hỗ trợ `EntityRepository` nữa. Nhưng đừng lo, cánh cửa này đóng thì vẫn có cánh khác mở ra, dù có chông gai hơn một tí, sau đây mình sẽ giúp các bạn tìm lại "ánh sáng" :

vì EntityRepository đã bị deprecated. Nên chúng ta sẽ tạo 1 module custom cho nó. Sau đây là các bước để hồi sinh lại EntityRepository ở typeorm version 0.3:

  1. Đầu tiên các bạn sẽ tạo file src/typeorm-ex/typeorm-ex.decorator.ts : mục đích dùng để thay thế cho EntityRepository decorator
import { SetMetadata } from "@nestjs/common";

export const TYPEORM_EX_CUSTOM_REPOSITORY = "TYPEORM_EX_CUSTOM_REPOSITORY";

export function CustomRepository(entity: Function): ClassDecorator {
  return SetMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, entity);
}
  1. Tiếp theo tạo file src/typeorm-ex/typeorm-ex.module.ts: đóng vai trò dùng để import DynamicModule Repository lúc khai báo trong module.
import { DynamicModule, Provider } from "@nestjs/common";
import { getDataSourceToken } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { TYPEORM_EX_CUSTOM_REPOSITORY } from "./typeorm-ex.decorator";

export class TypeOrmExModule {
  public static forCustomRepository<T extends new (...args: any[]) => any>(repositories: T[]): DynamicModule {
    const providers: Provider[] = [];

    for (const repository of repositories) {
      const entity = Reflect.getMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, repository);

      if (!entity) {
        continue;
      }

      providers.push({
        inject: [getDataSourceToken()],
        provide: repository,
        useFactory: (dataSource: DataSource): typeof repository => {
          const baseRepository = dataSource.getRepository<any>(entity);
          return new repository(baseRepository.target, baseRepository.manager, baseRepository.queryRunner);
        },
      });
    }

    return {
      exports: providers,
      module: TypeOrmExModule,
      providers,
    };
  }
}

  1. Để sử dụng, chúng ta sẽ import vào ArticleModule để sử dụng, và nhớ đừng quên import thêm ArticleEntity chúng ta đã tạo ra trước đó.
import { Module } from '@nestjs/common';
import { TypeOrmExModule } from 'src/typeorm-ex/typeorm-ex.module';
import { ArticleRepository } from './article.repository';
import { ArticleEntity } from './article.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  controllers: [],
  imports: [
    TypeOrmExModule.forCustomRepository([ArticleRepository]),
    TypeOrmModule.forFeature([ArticleEntity]),
  ],
  providers: []
})
export class ArticleModule {}

Như vậy là chúng ta đã chuẩn̉ bị xong phần dữ liệu, giờ chỉ còn lại là tạo các service và controller nữa là ổn.

4. Tạo Service, Controller cho `article` module

Như đã nói ở đầu bài viết, chúng ta sẽ tạo các chức năng CRUD cho article module.

  1. Tạo file src/article/dto/create-article.dto.ts dùng cho việc validation sau này. (Vì để dành thời gian cho phần chính nên mình sẽ không đề cập chi tiết phần này tại đây, mình sẽ có series khác dành cho phần này)
export class CreateArticleDTO {
    readonly title: string;
    readonly content: string;
    readonly author: string;
    readonly publicationDate: string;
}
  1. Tạo service ArticleService :
# tạo service article.service.ts và skip tạo file test
nest g s article --no-spec 

131pEN7W0zi3N5Dp_BUPZK_Ui8SvPPY2Y

Lệnh sẽ tạo ra file src/article/article.service.ts và tự động providers vào ArticleModule, sau đó chúng ta update lại nội dung:

import { BadRequestException, Injectable } from '@nestjs/common';
import { ArticleRepository } from './article.repository';
import { ArticleEntity } from './article.entity';
import { CreateArticleDTO } from './dto/create-article.dto';

@Injectable()
export class ArticleService {
    public constructor(
        private articleRepository: ArticleRepository,
    ) {}

    async all(): Promise<ArticleEntity[]> {
      return this.articleRepository.find();
    }

    async findOne(id: number): Promise<ArticleEntity> {
      return this.articleRepository.findOneBy({ id });
    }
  
    async create(dto: CreateArticleDTO): Promise<ArticleEntity> {
      let article: ArticleEntity = this.articleRepository.create(dto);
      return this.articleRepository.save(article);
    }
  
    async update(id: number, dto: CreateArticleDTO): Promise<ArticleEntity>     {
      const article = await this.findOne(id);
      if (!article) {
        throw new BadRequestException('Article Not Found')
      }

      return this.articleRepository.save({ ...article, ...dto })
    }

    async delete(id: number): Promise<void> {
      const article = await this.findOne(id);
      if (!article) {
        throw new BadRequestException('Article Not Found')
      }

      this.articleRepository.delete(id);
    }
}

  1. Tạo controller ArticleController :
# tạo controller article.controller.ts và skip tạo file test
nest g co article --no-spec 

1qCoIq47El8M0mlRJzl0VtOVVQO_BlIZB

Lệnh sẽ tạo ra file src/article/article.controller.ts và tự động add ArticleController vào ArticleModule, sau đó chúng ta update lại nội dung file:


import { Controller, Get, Res, HttpStatus, Param, NotFoundException, Post, Body, Put, Query, Delete, HttpCode } from '@nestjs/common';
import { CreateArticleDTO } from './dto/create-article.dto';
import { ArticleService } from './article.service';

@Controller('articles')
export class ArticleController {

  constructor(private articleService: ArticleService) { }

  @Get()
  async index(@Res() res) {
    const articles = await this.articleService.all();
    return res.status(HttpStatus.OK).json(articles);
  }

  @Get(':id')
  async show(@Param('id') id: number, @Res() res) {
    const article = await this.articleService.findOne(id);
    if (!article) {
      throw new NotFoundException('Article does not exist!');
    }
    return res.status(HttpStatus.OK).json(article);

  }

  @Post('')
  async create(@Body() dto: CreateArticleDTO, @Res() res) {
    const article = await this.articleService.create(dto);
    return res.status(HttpStatus.CREATED).json(article);
  }

  @Put(':id')
  async update(
    @Param('id') id,
    @Body() dto: CreateArticleDTO,
    @Res() res,
  ) {
    const article = await this.articleService.update(id, dto);
    return res.status(HttpStatus.OK).json(article);
  }

  @Delete(':id')
  async delete(@Param('id') id: number, @Res() res) {
    await this.articleService.delete(id);
    return res.status(HttpStatus.NO_CONTENT).json([]);
  }
}

5. Chạy và xem thành quả.

Chúng ta sẽ dùng postman để kiểm tra kết quả đạt được:

  1. Tạo article

1Zvk5ukuBhoVwYAr_uO7wvyng-fQTAr7I

  1. Get Detail article
    1lk7OgvsdEpnmTGxxmUkAu9JzCjNm1YGo

  2. Delete article
    1vxh6pPk26SidBcP-8ijugZSevTSOsgUi

  3. Get List article
    1r1lYNTmAbtCjvqHuRckfgSQUFo-xTmIx

  4. Update article
    18XpD4kptO6xecGgAM95-CAa6vItwHRG9

  5. Update article Not found
    1mG7ylNhQjB9-f1OshTXhJKi5JZcY6tpH

III. Tổng kết

Bằng việc sử dụng NestJS, chúng ta đã xây dựng một hệ thống API mạnh mẽ, có thể mở rộng và dễ bảo trì. NestJS cho phép chúng ta tận dụng các tính năng của TypeScript và các khái niệm của Angular để xây dựng các ứng dụng server-side hiệu suất cao.

Với PostgreSQL và TypeORM, chúng ta đã tạo được một hệ thống quản lý cơ sở dữ liệu ổn định và linh hoạt. TypeORM cho phép chúng ta sử dụng các khái niệm của đối tượng trong lập trình để tương tác với cơ sở dữ liệu một cách dễ dàng.

Tiếp đến, chúng ta sẽ chuyển sang phần 2 của series. Trong phần này, chúng ta sẽ tìm hiểu cách cấu hình và cài đặt Auth0 để xác thực NestJS API của chúng ta. Auth0 là một dịch vụ quản lý xác thực và ủy quyền rất mạnh mẽ, cho phép chúng ta xây dựng các tính năng đăng nhập, đăng ký và quản lý người dùng một cách an toàn và bảo mật.

Với Auth0, chúng ta sẽ có khả năng xác thực người dùng và cấp quyền truy cập vào các tài nguyên của ứng dụng. Điều này giúp chúng ta bảo vệ dữ liệu và đảm bảo rằng chỉ những người được phép mới có thể truy cập và sử dụng ứng dụng của chúng ta.

Với phần 2 sắp tới, chúng ta sẽ tiếp tục khám phá các khía cạnh thú vị và quan trọng của việc phát triển ứng dụng. Hãy sẵn sàng để đón nhận những kiến thức mới và trải nghiệm học tập tiếp theo nào!