DEV Community

NextjsVietnam
NextjsVietnam

Posted on

NestJS Course Lesson 03 - Controllers & Views

Source Code

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

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

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
# 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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

pet.module.ts

// src/pet/pet.module.ts

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

@Module({})
export class PetModule {}
Enter fullscreen mode Exit fullscreen mode
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}`;
   }
}
Enter fullscreen mode Exit fullscreen mode

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

image

image

image

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}`;
   }
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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}`;
   }
}
Enter fullscreen mode Exit fullscreen mode

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'); %>
Enter fullscreen mode Exit fullscreen mode

image

You can find all the related source code here:

And we have the following result:

image

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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode
// pet-dto.ts

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

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

export { CreatePetCategoryDto };
Enter fullscreen mode Exit fullscreen mode
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}`;
   }
}
Enter fullscreen mode Exit fullscreen mode
<%- 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'); %>
Enter fullscreen mode Exit fullscreen mode

And we get 3 states of the form as shown below

image
image
image

Feel free to read the full courses at NestJS Course Lesson 03 - Controllers & Views

Top comments (0)