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 pgCreating 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,
};
}
}



