Source Code
Lesson 03
- Learn about the module in NestJS - the application to build a directory structure for Pet Website
- Learn about EJS and how to create generic layouts
- Working with forms and checking input data
Overview
MVC pattern
Above the data flow model from the time the user makes the request until the result is received.
- Step 1: Controller receives data from User (www/form-data, multiplart/form-data, uri segements, query params, headers, ...)
- Step 2: Call to Service to request corresponding business processing, input is data from user
- Step 3: Service makes calls to Model to read/write corresponding data
- Step 4: The model reads/writes the corresponding data in the database
- Step 5: The service sends back to the controller the corresponding data that has been read/written/processed
- Step 6: The controller reads the corresponding template for the interface combined with the data received from the service to render the view to the user.
- Step 7: After rendering the corresponding view, the controller sends this result back to the user -> HTML/JSON, ...
Apply this MVC architecture to the project and divide the project by module
Practice
- Build the Pet module
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}`;
}
}
At this step NestJS provides some syntax to declare handler for each path
Now try to run your application again and see what we have
For 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}`;
}
}
Let's start with the form to create a pet category
- Integration with bootstrap (https://getbootstrap.com/docs/5.0/getting-started/download/)
- Use ejs partial (separate common parts of the website - header, footer, and reuse in different templates)
- Create routes
- Connect with view
- Get data from the form and process the results (fake data) The view folder structure will look like this:
views\pet\admin\manage-pet-category\create.ejs
So when using it, we just need to point the path to the template file located in the view directory
@Render("pet/admin/manage-pet-category/create")
After using some examples available at bootstrap we can use the template as below:
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}`;
}
}
In which the header, footer will contain the shared elements in the 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:
And we have the following result:
Okie and let's go to the next step, let's design a form to input and process data for 1 PetCategory
Notice that we have 3 use cases for the same view create form of admin pet category:
- Create New Pet Category
- Create New Pet Category success/failure
- Edit Pet Category
- Update Pet Categeory success/failure
Some constraints of this form:
- Pet category only has title
- Pet category title cannot be left blank
- Pet category title cannot be longer than 150 characters
# to support multipart/form-data
npm install nestjs-form-data --save
# to support data validation and transformation
npm install class-transformer reflect-metadata --save
To use multiplat/form-data, we need to import the NestJSFormData module as shown below.
By default NestJS is configured to only support json
// 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 {
properties,
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'); %>
And we get 3 states of the form as shown below
Feel free to read the full courses at NestJS Course Lesson 03 - Controllers & Views
Top comments (0)