NestJS Course Lesson 03 - Controllers & Views
Tags: nestjs, nestjstutorial, mvc, nextjsvietnam
Published: 2023-07-04 13:51:10

## Source Code

- [Lession 03 Source Code](https://github.com/misostack/nestjs-tutorial-2023/tree/lession03)

## Lesson 03

1. Learn about the module in NestJS - the application to build a directory structure for Pet Website
2. Learn about EJS and how to create generic layouts
3. Working with forms and checking input data

### Overview

#### MVC pattern

![image](https://user-images.githubusercontent.com/31009750/250263049-03852064-b404-40a9-8292-8d14cdf48138.png)

Above the data flow model from the time the user makes the request until the result is received.

1. Step 1: Controller receives data from User (www/form-data, multiplart/form-data, uri segements, query params, headers, ...)
2. Step 2: Call to Service to request corresponding business processing, input is data from user
3. Step 3: Service makes calls to Model to read/write corresponding data
4. Step 4: The model reads/writes the corresponding data in the database
5. Step 5: The service sends back to the controller the corresponding data that has been read/written/processed
6. 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.
7. 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

![image](https://user-images.githubusercontent.com/31009750/250264066-b17799f6-b81f-4068-8f77-e90b3221bd27.png)

### Practice

1. 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

```bash
# 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**

```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**

```ts
// src/pet/pet.module.ts

import { Module } from "@nestjs/common";

@Module({})
export class PetModule {}
```

```ts
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**

- [Reference about routes, params for controllers in NestJS](https://docs.nestjs.com/controllers#route-parameters)

Now try to run your application again and see what we have

![image](https://user-images.githubusercontent.com/31009750/250266534-82dd679a-1b8c-4785-8360-5ca09862b12a.png)

![image](https://user-images.githubusercontent.com/31009750/250266589-680113f5-8baa-46a8-b0a3-57f16a9ab2bc.png)

![image](https://user-images.githubusercontent.com/31009750/250266598-8844188c-d6d9-463f-8fdf-adb597a3bbeb.png)

For admin

```ts
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

```ts
@Render("pet/admin/manage-pet-category/create")
```

After using some examples available at bootstrap we can use the template as below:

```ts
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
```html
<%- include('layouts/admin/header'); %>
<h1>Manage Pet Category - Create New Pet Category</h1>
<%- include('layouts/admin/footer'); %>
```

![image](https://user-images.githubusercontent.com/31009750/250268918-ee065a07-00fe-4036-8035-280fdd3345c3.png)

You can find all the related source code here:

- [Assets](https://github.com/misostack/nestjs-tutorial-2023/tree/lession03/public/assets)
- [Layouts](https://github.com/misostack/nestjs-tutorial-2023/tree/lession03/views/layouts/admin)

And we have the following result:

![image](https://user-images.githubusercontent.com/31009750/250269750-766734ec-d18f-4c5f-b713-26a6e594bbf7.png)

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

```sh
# 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

```ts
// 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 {}
```

```ts
// pet-dto.ts

import { IsNotEmpty, MaxLength } from "class-validator";

class CreatePetCategoryDto {
   @MaxLength(50)
   @IsNotEmpty()
   title: string;
}

export { CreatePetCategoryDto };
```
```ts
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}`;
   }
}
```
```html
<%- 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

![image](https://user-images.githubusercontent.com/31009750/250274557-9a8d1677-7737-4127-b8d7-4ad1ca7404c0.png)
![image](https://user-images.githubusercontent.com/31009750/250274563-3952ec74-8aae-49ca-a093-39d2997eb8ed.png)
![image](https://user-images.githubusercontent.com/31009750/250274577-33997f54-815c-4600-9dd3-acfc1b96fb38.png)

Feel free to read the full courses at [NestJS Course Lesson 03 - Controllers & Views](https://nextjsvietnam.com/post/khoa-hoc-nestjs-bai03/)