☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Recipes

Hot Reload

Bản dịch từ docs.nestjs.com/recipes/hot-reload

Yếu tố ảnh hưởng lớn nhất đến quá trình khởi động (bootstrapping) của ứng dụng là quá trình compile TypeScript. May mắn là, với webpack HMR (Hot-Module Replacement), chúng ta không cần compile lại toàn bộ project mỗi khi có thay đổi. Điều này giảm đáng kể thời gian cần để khởi tạo ứng dụng, và giúp việc phát triển lặp (iterative development) dễ dàng hơn nhiều.

Cảnh báo Lưu ý là webpack sẽ không tự động copy asset của bạn (ví dụ file graphql) vào thư mục dist. Tương tự, webpack không tương thích với glob static path (ví dụ property entities trong TypeOrmModule).

Cảnh báo Từ NestJS v12, webpack builder đã bị deprecated (không còn khuyến nghị dùng) và Rspack trở thành bundler mặc định cho monorepo. Bài này nhắm tới các project dùng webpack với CommonJS, đó là lý do các ví dụ dưới đây dùng module.hot và một lời gọi bootstrap() thông thường, thay vì await ở top-level theo kiểu ESM như ở các phần khác của tài liệu này. Với project mới, nên dùng --builder rspack.

Ghi chú người dịch Nếu bạn từng dùng nodemon với Express, hot reload ở đây khác về bản chất: nodemon chỉ restart lại toàn bộ process Node khi có file thay đổi (tức chạy lại node index.js từ đầu), còn HMR của webpack thay thế (replace) các module đã thay đổi ngay trong process đang chạy, giữ lại state hiện có (ví dụ connection pool, cache trong memory). Vì thế tốc độ nhanh hơn hẳn, nhưng đổi lại logic bootstrap phức tạp hơn — bạn phải tự lo việc đóng app cũ trước khi app mới bind lại port, như đoạn module.hot.dispose ở dưới.

Dùng CLI

Nếu bạn dùng Nest CLI, quá trình cấu hình khá đơn giản. CLI bọc (wrap) webpack, cho phép dùng HotModuleReplacementPlugin.

Cài đặt

Đầu tiên, cài các package cần thiết:

$ npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack

Gợi ý Nếu bạn dùng Yarn Berry (không phải Yarn classic), cài package webpack-pnp-externals thay cho webpack-node-externals.

Cấu hình

Sau khi cài đặt xong, tạo file webpack-hmr.config.js ở thư mục gốc của ứng dụng.

const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
    ],
  };
};

Gợi ý Với Yarn Berry (không phải Yarn classic), thay vì dùng nodeExternals trong property cấu hình externals, hãy dùng WebpackPnpExternals từ package webpack-pnp-externals: WebpackPnpExternals({ exclude: ['webpack/hot/poll?100'] }).

Hàm này nhận object chứa cấu hình webpack mặc định làm đối số đầu tiên, và tham chiếu tới package webpack gốc mà Nest CLI đang dùng làm đối số thứ hai. Ngoài ra, nó trả về một cấu hình webpack đã được sửa đổi, có thêm plugin HotModuleReplacementPlugin, WatchIgnorePlugin, và RunScriptWebpackPlugin.

Hot-Module Replacement

Để bật HMR, mở file entry của ứng dụng (main.ts) và thêm các đoạn code liên quan đến webpack sau:

declare const module: any;

async function bootstrap() {
  if (module.hot?.data?.closePromise) {
    // đợi instance ứng dụng trước đó đóng hoàn toàn
    await module.hot.data.closePromise;
  }

  const app = await NestFactory.create(AppModule, {
    forceCloseConnections: !!module.hot,
  });
  await app.listen(process.env.PORT ?? 3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose((data: any) => {
      data.closePromise = app.close();
    });
  }
}
bootstrap();

Gợi ý app.close() là bất đồng bộ (asynchronous), nhưng webpack không đợi (await) callback dispose(). Việc lưu promise trả về vào module.hot.data cho phép instance ứng dụng tiếp theo đợi nó xong trước khi bind lại vào port, và (kết hợp với forceCloseConnections) giúp tránh lỗi EADDRINUSE khi reload.

Ghi chú người dịch EADDRINUSE chính là lỗi “address already in use” quen thuộc khi bạn chạy hai process cùng listen một port — ở đây nó xảy ra vì webpack thay module mới vào ngay lập tức, trong khi app cũ (đang giữ port) vẫn có thể còn đang đóng connection dở dang (ví dụ connection tới DB, WebSocket đang mở). Đây là cạm bẫy dễ gặp nếu bạn copy code này nhưng bỏ qua đoạn closePromise/forceCloseConnections: mọi thứ vẫn chạy được vài lần reload đầu, rồi bất ngờ báo lỗi bind port khi có connection “cứng đầu” chưa kịp đóng.

Để đơn giản hóa việc chạy, thêm một script vào file package.json.

"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"

Giờ chỉ cần mở command line và chạy lệnh sau:

$ npm run start:dev

Không dùng CLI

Nếu bạn không dùng Nest CLI, việc cấu hình sẽ phức tạp hơn một chút (cần nhiều bước thủ công hơn).

Cài đặt

Đầu tiên, cài các package cần thiết:

$ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader run-script-webpack-plugin

Gợi ý Nếu bạn dùng Yarn Berry (không phải Yarn classic), cài package webpack-pnp-externals thay cho webpack-node-externals.

Cấu hình

Sau khi cài đặt xong, tạo file webpack.config.js ở thư mục gốc của ứng dụng.

const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = {
  entry: ['webpack/hot/poll?100', './src/main.ts'],
  target: 'node',
  externals: [
    nodeExternals({
      allowlist: ['webpack/hot/poll?100'],
    }),
  ],
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [new webpack.HotModuleReplacementPlugin(), new RunScriptWebpackPlugin({ name: 'server.js', autoRestart: false })],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'server.js',
  },
};

Gợi ý Với Yarn Berry (không phải Yarn classic), thay vì dùng nodeExternals trong property cấu hình externals, hãy dùng WebpackPnpExternals từ package webpack-pnp-externals: WebpackPnpExternals({ exclude: ['webpack/hot/poll?100'] }).

Cấu hình này nói cho webpack biết một số điều cần thiết về ứng dụng của bạn: vị trí file entry, thư mục nào sẽ dùng để chứa các file đã compile, và loại loader nào sẽ dùng để compile source file. Nói chung, bạn có thể dùng file này gần như nguyên bản, dù không hiểu hết mọi option.

Hot-Module Replacement

Để bật HMR, mở file entry của ứng dụng (main.ts) và thêm các đoạn code liên quan đến webpack sau:

declare const module: any;

async function bootstrap() {
  if (module.hot?.data?.closePromise) {
    // đợi instance ứng dụng trước đó đóng hoàn toàn
    await module.hot.data.closePromise;
  }

  const app = await NestFactory.create(AppModule, {
    forceCloseConnections: !!module.hot,
  });
  await app.listen(process.env.PORT ?? 3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose((data: any) => {
      data.closePromise = app.close();
    });
  }
}
bootstrap();

Gợi ý app.close() là bất đồng bộ (asynchronous), nhưng webpack không đợi (await) callback dispose(). Việc lưu promise trả về vào module.hot.data cho phép instance ứng dụng tiếp theo đợi nó xong trước khi bind lại vào port, và (kết hợp với forceCloseConnections) giúp tránh lỗi EADDRINUSE khi reload.

Để đơn giản hóa việc chạy, thêm một script vào file package.json.

"start:dev": "webpack --config webpack.config.js --watch"

Giờ chỉ cần mở command line và chạy lệnh sau:

$ npm run start:dev

Ví dụ

Bạn có thể xem một ví dụ chạy được tại đây.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.