Lesson 25-NestJS Model-View-Controller

While NestJS does not directly follow the traditional MVC (Model-View-Controller) architecture, you can leverage NestJS features to build an architecture resembling MVC. In NestJS, controllers and services act similarly to controllers and models in MVC, while template engines or front-end frameworks serve as the view layer.

Creating a Model

The model layer typically handles data and business logic. In NestJS, this is usually implemented using services or entities.

// cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  private cats = [];

  create(cat: any): void {
    this.cats.push(cat);
  }

  findAll(): any[] {
    return this.cats;
  }
}

Creating a Controller

The controller layer handles HTTP requests and responses, interacting with the model layer to retrieve data and passing it to the view layer.

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  create(@Body() cat: any): any {
    this.catsService.create(cat);
    return cat;
  }

  @Get()
  findAll(): any[] {
    return this.catsService.findAll();
  }
}

Creating a View

The view layer is responsible for displaying data. In NestJS, you can use template engines like EJS, Pug, or Handlebars to render views.

// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
import { APP_VIEW, APP_TEMPLATE_OPTIONS } from '@nestjs/core';

@Module({
  imports: [],
  controllers: [CatsController],
  providers: [
    CatsService,
    {
      provide: APP_VIEW,
      useValue: 'ejs', // Or 'pug', 'handlebars', etc.
    },
    {
      provide: APP_TEMPLATE_OPTIONS,
      useValue: {
        root: 'views',
      },
    },
  ],
})
export class AppModule {}

View File:

<!-- views/cats/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Cats</title>
</head>
<body>
  <h1>Cats List</h1>
  <% cats.forEach(function(cat) { %>
    <p><%= cat.name %> - <%= cat.age %></p>
  <% }); %>
</body>
</html>

Rendering Views

In the controller, you can use the render method to render views.

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  create(@Body() cat: any): any {
    this.catsService.create(cat);
    return cat;
  }

  @Get()
  findAll(): any[] {
    const cats = this.catsService.findAll();
    return { cats };
  }

  @Get('view')
  viewCats(): any {
    const cats = this.catsService.findAll();
    return {
      view: 'cats/index',
      cats,
    };
  }
}

Using ORM (Object-Relational Mapping)

In real-world applications, the model layer often needs to interact with a database. You can use ORM (Object-Relational Mapping) tools like TypeORM or Sequelize to simplify database operations.

Installing TypeORM and PostgreSQL Driver:

npm install typeorm pg

Creating an Entity:

// entities/cat.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  age: number;
}

Configuring TypeORM:

// ormconfig.js
module.exports = {
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'password',
  database: 'nest_cats',
  entities: ['entities/*.entity{.ts,.js}'],
  synchronize: true, // Disable auto-sync in production
};

Updating the Service:

// cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cat } from '../entities/cat.entity';

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private catsRepository: Repository<Cat>,
  ) {}

  async create(cat: any): Promise<Cat> {
    const createdCat = this.catsRepository.create(cat);
    return this.catsRepository.save(createdCat);
  }

  async findAll(): Promise<Cat[]> {
    return this.catsRepository.find();
  }
}

Separating Business Logic

As applications grow more complex, you may need to separate business logic from services to maintain code clarity and maintainability.

Creating a Domain Service:

// domain/services/cat.service.ts
import { Injectable } from '@nestjs/common';
import { CatsService } from '../services/cats.service';

@Injectable()
export class CatDomainService {
  constructor(private catsService: CatsService) {}

  async createCat(name: string, age: number): Promise<void> {
    const cat = { name, age };
    await this.catsService.create(cat);
  }

  async getAllCats(): Promise<any[]> {
    return this.catsService.findAll();
  }
}

Updating the Controller:

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatDomainService } from '../domain/services/cat.service';

@Controller('cats')
export class CatsController {
  constructor(
    private catsService: CatsService,
    private catDomainService: CatDomainService,
  ) {}

  @Post()
  async create(@Body() cat: any): Promise<any> {
    await this.catDomainService.createCat(cat.name, cat.age);
    return cat;
  }

  @Get()
  async findAll(): Promise<any[]> {
    return this.catDomainService.getAllCats();
  }

  @Get('view')
  viewCats(): any {
    const cats = this.catsService.findAll();
    return {
      view: 'cats/index',
      cats,
    };
  }
}
Share your love