Khóa học NestJS Bài 03 - Controllers & Views
Post Date : 2023-06-21T14:59:42+07:00
Modified Date : 2023-06-21T14:59:42+07:00
Category: nestjs-tutorial
Tags: nestjs , nestjs-pet-website
Source Code
Bài 03
- Tìm hiểu về module trong NestJS - ứng dụng xây dựng cấu trúc thư mục cho Pet Website
- Tìm hiểu về EJS và cách tạo layout chung
- Làm việc với form và kiểm tra dữ liệu đầu vào
Tổng quan
Mô hình MVC
Phía trên mô hình luồng dữ liệu từ khi người dùng thực hiện yêu cầu cho đến khi nhận được kết quả.
- Bước 1 : Controller nhận dữ liệu từ User (www/form-data, multiplart/form-data, uri segements, query params, headers, …)
- Bước 2 : Gọi tới Service để yêu cầu xử lý nghiệp vụ tương ứng, input là data từ user
- Bước 3 : Service thực hiện gọi tới Model để đọc/ghi dữ liệu tương ứng
- Bước 4: Model thực hiện đọc/ghi dữ liệu tương ứng trong database
- Bước 5: Service gửi trả cho controller dữ liệu đã được đọc/ghi/xử lý nghiệp vụ tương ứng
- Bước 6: Controller đọc template tương ứng cho phần giao diện kết hợp với data nhận được từ service để render phần view cho người dùng.
- Bước 7: Sau khi render được phần view tương ứng, controller gửi kết quả này lại cho người dùng -> HTML/JSON, …
Áp dụng kiến trúc MVC này vào dự án đồng thời phân chia dự án theo từng module
Thực hành
- Xây dựng module Pet
1.1. Controllers
- PetController - /pets - /pets/:petId
- ManagePetController /admin/pets/
- ManagePetCategoryController /admin/pet-categories
- ManagePetAttributeController / admin/pet-attributes
1.2. Services
- PetService
- PetCategoryService
- PetAttributeService
1.3. Models
- Pet
- PetCategory
- PetAttribute
# let's create a pet module
nest g module pet
# let's create controllers
nest g controller pet/controllers/pet --flat
# for admin pages
nest g controller pet/controllers/admin/manage-pet --flat
nest g controller pet/controllers/admin/manage-pet-category --flat
nest g controller pet/controllers/admin/manage-pet-attribute --flat
app.module.ts
// src/app.module.ts
import { Module } from "@nestjs/common";
import { ServeStaticModule } from "@nestjs/serve-static";
import { join } from "path";
import { PetModule } from "./pet/pet.module";
@Module({
imports: [
// public folder
ServeStaticModule.forRoot({
rootPath: join(process.cwd(), "public"),
serveRoot: "/public",
}),
PetModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
pet.module.ts
// src/pet/pet.module.ts
import { Module } from "@nestjs/common";
@Module({})
export class PetModule {}
import { Controller, Get, Param } from "@nestjs/common";
@Controller("pets")
export class PetController {
@Get("")
getList() {
return "Pet List";
}
@Get(":id")
getDetail(@Param() { id }: { id: number }) {
return `Pet Detail ${id}`;
}
}
Tại bước này NestJS cung cấp 1 số cú pháp để khai báo handler cho mỗi path
Đến đây hãy thử chạy lại ứng dụng của bạn xem chúng ta có gì nào
Cho admin
import { Controller, Get, Param } from "@nestjs/common";
@Controller("admin/pets")
export class ManagePetController {
@Get("")
getList() {
return "admin pet list";
}
@Get(":id")
getDetail(@Param() { id }: { id: string }) {
return `admin pet detail ${id}`;
}
}
import { Controller, Get, Param } from "@nestjs/common";
@Controller("admin/pet-categories")
export class ManagePetCategoryController {
@Get("")
getList() {
return "admin pet categories";
}
@Get(":id")
getDetail(@Param() { id }: { id: string }) {
return `admin pet category detail ${id}`;
}
}
import { Controller, Get, Param } from "@nestjs/common";
@Controller("admin/pet-attributes")
export class ManagePetAttributeController {
@Get("")
getList() {
return "admin pet attribute list";
}
@Get(":id")
getDetail(@Param() { id }: { id: string }) {
return `admin pet attribute detail ${id}`;
}
}
Chúng ta hãy bắt đầu với form tạo 1 pet category
- Tích hợp với bootstrap (https://getbootstrap.com/docs/5.0/getting-started/download/)
- Sử dụng ejs partial ( tách các phần chung của trang web - header, footer, và sử dụng lại trong từng template khác nhau)
- Tạo route
- Kết nối với view
- Nhận dữ liệu từ form và xử lý kết quả (fake data)
Cấu trúc thư mục view sẽ có dạng như sau
views\pet\admin\manage-pet-category\create.ejs
Do đó khi sử dụng ta chỉ cần chỉ đường dẫn tới file template nằm trong thư mục view
@Render("pet/admin/manage-pet-category/create")
Sau khi sử dụng 1 số example có sẵn tại bootstrap ta có thể sử dụng template như bên dưới:
import { Controller, Get, Param, Post, Render } from "@nestjs/common";
@Controller("admin/pet-categories")
export class ManagePetCategoryController {
@Get("")
getList() {
return "admin pet categories";
}
@Get("create")
@Post("create")
@Render("pet/admin/manage-pet-category/create")
create() {
// a form
return {};
}
@Get(":id")
getDetail(@Param() { id }: { id: string }) {
return `admin pet category detail ${id}`;
}
}
Trong đó phần header, footer sẽ chứa những thành phần dùng chung trong template
<%- include('layouts/admin/header'); %>
<h1>Manage Pet Category - Create New Pet Category</h1>
<%- include('layouts/admin/footer'); %>
You can find all the related source code here:
Và chúng ta có kết quả như sau:
Okie và hãy tới bước tiếp theo nào, hãy thiết kế 1 form để nhập và xử lý dữ liệu cho 1 PetCategory
Hãy chú ý rằng chúng ta có 3 trường hợp sử dụng cùng 1 view create form của admin pet category:
- Create New Pet Category
- Create New Pet Category thành công/thất bại
- Edit Pet Category
- Update Pet Categeory thành công/thất bại
Một số ràng buộc của form này:
- Pet category chỉ có title
- Pet category title không được để trống
- Pet category title không được dài hơn 150 kí tự
# to support multipart/form-data
npm install nestjs-form-data --save
# to support data validation and transformation
npm install class-transformer reflect-metadata --save
Để sử dụng được multiplart/form-data, chúng ta cần import module NestJSFormData như bên dưới. Do mặc định NestJS được cấu hình chỉ để support json d
// src/pet/pet.module.ts
import { Module } from "@nestjs/common";
import { PetController } from "./controllers/pet.controller";
import { ManagePetController } from "./controllers/admin/manage-pet.controller";
import { ManagePetCategoryController } from "./controllers/admin/manage-pet-category.controller";
import { ManagePetAttributeController } from "./controllers/admin/manage-pet-attribute.controller";
import { NestjsFormDataModule } from "nestjs-form-data";
@Module({
imports: [NestjsFormDataModule],
controllers: [
PetController,
ManagePetController,
ManagePetCategoryController,
ManagePetAttributeController,
],
})
export class PetModule {}
// pet-dto.ts
import { IsNotEmpty, MaxLength } from "class-validator";
class CreatePetCategoryDto {
@MaxLength(50)
@IsNotEmpty()
title: string;
}
export { CreatePetCategoryDto };
import { Body, Controller, Get, Param, Post, Render } from "@nestjs/common";
import { CreatePetCategoryDto } from "src/pet/dtos/pet-dto";
import { plainToInstance } from "class-transformer";
import { validate, ValidationError } from "class-validator";
import { FormDataRequest } from "nestjs-form-data";
const transformError = (error: ValidationError) => {
const { property, constraints } = error;
return {
property,
constraints,
};
};
@Controller("admin/pet-categories")
export class ManagePetCategoryController {
@Get("")
getList() {
return "admin pet categories";
}
@Get("create")
@Render("pet/admin/manage-pet-category/create")
view_create() {
// a form
return {
data: {
mode: "create",
},
};
}
@Post("create")
@Render("pet/admin/manage-pet-category/create")
@FormDataRequest()
async create(@Body() createPetCategoryDto: CreatePetCategoryDto) {
const data = {
mode: "create",
};
// validation
const object = plainToInstance(CreatePetCategoryDto, createPetCategoryDto);
const errors = await validate(object, {
stopAtFirstError: true,
});
if (errors.length > 0) {
Reflect.set(data, "error", "Please correct all fields!");
const responseError = {};
errors.map((error) => {
const rawError = transformError(error);
Reflect.set(
responseError,
rawError.property,
Object.values(rawError.constraints)[0]
);
});
Reflect.set(data, "errors", responseError);
return { data };
}
// set value and show success message
Reflect.set(data, "values", object);
Reflect.set(
data,
"success",
`Pet Category : ${object.title} has been created!`
);
// success
return { data };
}
@Get(":id")
getDetail(@Param() { id }: { id: string }) {
return `admin pet category detail ${id}`;
}
}
<%- include('layouts/admin/header'); %>
<section class="col-6">
<form method="post" enctype="multipart/form-data">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<% if (data.mode === 'create') { %> New Pet Category <% } %>
</h5>
<!-- error -->
<% if (data.error){ %>
<div class="alert alert-danger" role="alert"><%= data.error %></div>
<% } %>
<!-- success -->
<% if (data.success){ %>
<div class="alert alert-success" role="alert"><%= data.success %></div>
<% } %>
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<div class="input-group has-validation">
<input
type="text"
class="form-control <%= data.errors && data.errors['title'] ? 'is-invalid': '' %>"
id="title"
name="title"
value="<%= data.values && data.values['title'] %>"
placeholder="Pet Category Title"
/>
<% if (data.errors && data.errors['title']) { %>
<div id="validationServerUsernameFeedback" class="invalid-feedback">
<%= data.errors['title'] %>
</div>
<% } %>
</div>
</div>
</div>
<% if(!data.success) { %>
<div class="mb-3 col-12 text-center">
<button type="submit" class="btn btn-primary">Save</button>
</div>
<% } %>
</div>
</form>
</section>
<%- include('layouts/admin/footer'); %>
Và chúng ta có được 3 trạng thái của form như bên dưới