Theo mặc định, Nest dùng thư viện Express bên dưới. Do đó, mọi kỹ thuật để dùng pattern MVC (Model-View-Controller) trong Express đều áp dụng được cho Nest.
Đầu tiên, hãy scaffold một ứng dụng Nest đơn giản bằng công cụ CLI:
$ npm i -g @nestjs/cli
$ nest new project
Để tạo một app MVC, chúng ta cũng cần một template engine để render các HTML view:
$ npm install --save hbs
Chúng ta dùng engine hbs (Handlebars), nhưng bạn có thể dùng bất kỳ engine nào phù hợp với yêu cầu của bạn. Sau khi cài đặt xong, ta cần cấu hình Express instance bằng code sau:
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'node:path';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
);
app.useStaticAssets(join(import.meta.dirname, '..', 'public'));
app.setBaseViewsDir(join(import.meta.dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
import { NestFactory } from '@nestjs/core';
import { join } from 'node:path';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
);
app.useStaticAssets(join(import.meta.dirname, '..', 'public'));
app.setBaseViewsDir(join(import.meta.dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
Chúng ta đã báo cho Express biết rằng thư mục public sẽ dùng để lưu static asset, views sẽ chứa template, và engine hbs nên được dùng để render HTML output.
Ghi chú người dịch Về bản chất, pattern MVC ở đây không có gì đặc trưng riêng của Nest cả — Nest chỉ đứng ra tổ chức lại controller (phần “C”) theo class/decorator, còn việc render view vẫn hoàn toàn dựa vào cơ chế template engine của Express (giống hệt cách bạn dùng res.render() trong một app Express thuần, hoặc Blade trong Laravel). Nếu bạn đang xây REST API trả JSON (phần lớn use case thực tế với Nest), bạn sẽ không cần tới chương này — nó chỉ hữu ích khi bạn thực sự muốn Nest render HTML phía server (SSR truyền thống) thay vì làm backend cho SPA/frontend riêng.
Render template (Template rendering)
Bây giờ, hãy tạo một thư mục views và một template index.hbs bên trong nó. Trong template, ta sẽ in ra một message được truyền từ controller:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>App</title>
</head>
<body>
{{ "{{ message }\}" }}
</body>
</html>
Tiếp theo, mở file app.controller và thay method root() bằng code sau:
import { Get, Controller, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index')
root() {
return { message: 'Hello world!' };
}
}
Trong code này, ta chỉ định template cần dùng trong decorator @Render(), và giá trị trả về của method route handler được truyền cho template để render. Chú ý rằng giá trị trả về là một object có property message, khớp với placeholder message ta đã tạo trong template.
Trong khi ứng dụng đang chạy, mở browser và truy cập http://localhost:3000. Bạn sẽ thấy message Hello world!.
Thêm layout (Adding a layout)
Engine hbs hỗ trợ layout — các template wrapper chung mà từng view riêng lẻ sẽ được render vào trong đó. Để dùng layout, set local variable layout bằng method setLocal() (wrapper của Nest quanh app.locals của Express). Hãy sửa đoạn code trước như sau:
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
);
app.setLocal('layout', 'layouts/app');
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
import { NestFactory } from '@nestjs/core';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
);
app.setLocal('layout', 'layouts/app');
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Tiếp theo, tạo một thư mục layouts và thêm file app.hbs với nội dung sau:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>App</title>
</head>
<body>
{{{body}}}
</body>
</html>
Sau đó, cập nhật file index.hbs thành:
{{ "{{ message }\}" }}
Cấu trúc file kết quả sẽ trông như sau:
Render template động (Dynamic template rendering)
Nếu logic ứng dụng cần quyết định động (dynamically) template nào sẽ được render, ta nên dùng decorator @Res(), và cung cấp tên view trong route handler, thay vì trong decorator @Render():
Gợi ý Khi Nest phát hiện decorator @Res(), nó sẽ inject response object đặc thù của thư viện (library-specific). Ta có thể dùng object này để render template một cách động. Tìm hiểu thêm về API của response object tại đây.
import { Get, Controller, Res, Render } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service.js';
@Controller()
export class AppController {
constructor(private appService: AppService) {}
@Get()
root(@Res() res: Response) {
return res.render(
this.appService.getViewName(),
{ message: 'Hello world!' },
);
}
}
Ví dụ (Example)
Một ví dụ hoạt động thực tế có sẵn tại đây.
Fastify
Như đã đề cập trong chương này, ta có thể dùng bất kỳ HTTP provider tương thích nào cùng với Nest. Một trong số đó là Fastify. Để tạo một ứng dụng MVC với Fastify, cài các package sau:
$ npm i --save @fastify/static @fastify/view handlebars
Các bước tiếp theo hầu như giống với quy trình đã dùng với Express, chỉ có một vài khác biệt nhỏ đặc thù cho platform này. Sau khi cài đặt xong, mở file main.ts và cập nhật nội dung của nó:
import { NestFactory } from '@nestjs/core';
import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './app.module.js';
import { join } from 'node:path';
import Handlebars from 'handlebars';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useStaticAssets({
root: join(import.meta.dirname, '..', 'public'),
prefix: '/public/',
});
app.setViewEngine({
engine: {
handlebars: Handlebars,
},
templates: join(import.meta.dirname, '..', 'views'),
});
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './app.module.js';
import { join } from 'node:path';
import Handlebars from 'handlebars';
async function bootstrap() {
const app = await NestFactory.create(AppModule, new FastifyAdapter());
app.useStaticAssets({
root: join(import.meta.dirname, '..', 'public'),
prefix: '/public/',
});
app.setViewEngine({
engine: {
handlebars: Handlebars,
},
templates: join(import.meta.dirname, '..', 'views'),
});
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
API của Fastify có một vài khác biệt, nhưng kết quả cuối cùng của các lệnh gọi method này là như nhau. Một khác biệt đáng chú ý là khi dùng Fastify, tên template bạn truyền vào decorator @Render() phải bao gồm cả phần đuôi file (file extension).
Đây là cách bạn thiết lập nó:
import { Get, Controller, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index.hbs')
root() {
return { message: 'Hello world!' };
}
}
Ngoài ra, bạn có thể dùng decorator @Res() để inject trực tiếp response và chỉ định view muốn render, như bên dưới:
import { Res } from '@nestjs/common';
import { FastifyReply } from 'fastify';
@Get()
root(@Res() res: FastifyReply) {
return res.view('index.hbs', { title: 'Hello world!' });
}
Trong khi ứng dụng đang chạy, mở browser và truy cập http://localhost:3000. Bạn sẽ thấy message Hello world!.
Ví dụ (Example)
Một ví dụ hoạt động thực tế có sẵn tại đây.