MVC en Nest

Intermedio
Nest
Nest
Actualizado: 15/06/2025

Controlador MVC con vista en Nest

El patrón MVC (Model-View-Controller) en NestJS permite separar la lógica de presentación de la lógica de negocio mediante el uso de vistas renderizadas del lado del servidor. A diferencia de los controladores que devuelven JSON para APIs REST, los controladores MVC devuelven HTML renderizado que se envía directamente al navegador del usuario.

Para implementar MVC en NestJS necesitamos configurar un motor de plantillas que procese nuestras vistas. Los motores más utilizados son Handlebars, EJS y Pug, siendo Handlebars una opción popular por su sintaxis clara y su amplio ecosistema.

Configuración del motor de plantillas

Primero instalamos las dependencias necesarias para trabajar con Handlebars:

npm install hbs
npm install @types/hbs --save-dev

En el archivo main.ts configuramos el motor de plantillas y especificamos la carpeta donde se almacenarán nuestras vistas:

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  // Configurar motor de plantillas
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('hbs');
  
  await app.listen(3000);
}
bootstrap();

Esta configuración establece que las vistas se encuentran en la carpeta views en la raíz del proyecto y utiliza Handlebars como motor de renderizado.

Estructura de carpetas para MVC

La estructura típica para un proyecto MVC en NestJS incluye:

src/
├── controllers/
├── services/
├── modules/
└── main.ts
views/
├── layouts/
│   └── main.hbs
├── partials/
└── pages/
    ├── home.hbs
    └── products.hbs
public/
├── css/
├── js/
└── images/

La carpeta views contiene nuestras plantillas, public almacena archivos estáticos como CSS y JavaScript, mientras que src mantiene la lógica del servidor.

Implementación de un controlador MVC

Un controlador MVC utiliza el decorador @Render() para especificar qué vista debe renderizar. Aquí implementamos un controlador para una tienda online:

import { Controller, Get, Render, Param } from '@nestjs/common';
import { ProductsService } from './products.service';

@Controller()
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get()
  @Render('pages/home')
  getHomePage() {
    const featuredProducts = this.productsService.getFeaturedProducts();
    return {
      title: 'Tienda Online - Inicio',
      featuredProducts,
      showBanner: true
    };
  }

  @Get('productos')
  @Render('pages/products')
  getProductsPage() {
    const products = this.productsService.getAllProducts();
    const categories = this.productsService.getCategories();
    
    return {
      title: 'Nuestros Productos',
      products,
      categories,
      totalProducts: products.length
    };
  }

  @Get('producto/:id')
  @Render('pages/product-detail')
  getProductDetail(@Param('id') id: string) {
    const product = this.productsService.getProductById(parseInt(id));
    const relatedProducts = this.productsService.getRelatedProducts(product.categoryId);
    
    return {
      title: `${product.name} - Detalle del Producto`,
      product,
      relatedProducts,
      breadcrumb: [
        { name: 'Inicio', url: '/' },
        { name: 'Productos', url: '/productos' },
        { name: product.name, url: null }
      ]
    };
  }
}

Cada método del controlador devuelve un objeto con datos que estarán disponibles en la vista correspondiente. El decorador @Render() especifica qué plantilla utilizar para renderizar la respuesta.

Creación de vistas con Handlebars

Las vistas en Handlebars utilizan una sintaxis de llaves dobles para mostrar datos dinámicos. Creamos la vista principal views/layouts/main.hbs:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}}</title>
    <link rel="stylesheet" href="/css/styles.css">
</head>
<body>
    <header>
        <nav>
            <a href="/">Inicio</a>
            <a href="/productos">Productos</a>
        </nav>
    </header>
    
    <main>
        {{{body}}}
    </main>
    
    <footer>
        <p>&copy; 2024 Tienda Online</p>
    </footer>
    
    <script src="/js/main.js"></script>
</body>
</html>

La vista de productos views/pages/products.hbs muestra la lista de productos:

<div class="products-container">
    <h1>{{title}}</h1>
    
    <div class="filters">
        <h3>Categorías</h3>
        {{#each categories}}
            <label>
                <input type="checkbox" value="{{id}}"> {{name}}
            </label>
        {{/each}}
    </div>
    
    <div class="products-grid">
        {{#each products}}
            <div class="product-card">
                <img src="{{image}}" alt="{{name}}">
                <h3>{{name}}</h3>
                <p class="price">${{price}}</p>
                <a href="/producto/{{id}}" class="btn">Ver Detalle</a>
            </div>
        {{/each}}
    </div>
    
    <p>Mostrando {{totalProducts}} productos</p>
</div>

Servir archivos estáticos

Para que las vistas puedan acceder a archivos CSS, JavaScript e imágenes, configuramos el servicio de archivos estáticos en main.ts:

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  // Configurar archivos estáticos
  app.useStaticAssets(join(__dirname, '..', 'public'));
  
  // Configurar vistas
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('hbs');
  
  await app.listen(3000);
}
bootstrap();

Esta configuración permite que los archivos en la carpeta public sean accesibles directamente desde el navegador mediante rutas como /css/styles.css o /js/main.js.

Manejo de formularios en MVC

Los controladores MVC también pueden procesar formularios HTML y redirigir a otras vistas. Implementamos un controlador para manejar el contacto:

@Controller('contacto')
export class ContactController {
  
  @Get()
  @Render('pages/contact')
  getContactForm() {
    return {
      title: 'Contacto',
      formData: {},
      errors: {}
    };
  }

  @Post()
  @Render('pages/contact')
  async submitContact(@Body() contactData: any) {
    try {
      // Validar datos del formulario
      const errors = this.validateContactForm(contactData);
      
      if (Object.keys(errors).length > 0) {
        return {
          title: 'Contacto',
          formData: contactData,
          errors,
          showErrors: true
        };
      }
      
      // Procesar formulario exitosamente
      await this.sendContactEmail(contactData);
      
      return {
        title: 'Contacto',
        formData: {},
        errors: {},
        successMessage: 'Mensaje enviado correctamente'
      };
      
    } catch (error) {
      return {
        title: 'Contacto',
        formData: contactData,
        errors: { general: 'Error al enviar el mensaje' }
      };
    }
  }
}

El formulario de contacto views/pages/contact.hbs muestra errores y mantiene los datos ingresados:

<div class="contact-form">
    <h1>{{title}}</h1>
    
    {{#if successMessage}}
        <div class="alert alert-success">{{successMessage}}</div>
    {{/if}}
    
    {{#if errors.general}}
        <div class="alert alert-error">{{errors.general}}</div>
    {{/if}}
    
    <form method="POST" action="/contacto">
        <div class="form-group">
            <label for="name">Nombre:</label>
            <input type="text" id="name" name="name" value="{{formData.name}}">
            {{#if errors.name}}
                <span class="error">{{errors.name}}</span>
            {{/if}}
        </div>
        
        <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" value="{{formData.email}}">
            {{#if errors.email}}
                <span class="error">{{errors.email}}</span>
            {{/if}}
        </div>
        
        <div class="form-group">
            <label for="message">Mensaje:</label>
            <textarea id="message" name="message">{{formData.message}}</textarea>
            {{#if errors.message}}
                <span class="error">{{errors.message}}</span>
            {{/if}}
        </div>
        
        <button type="submit">Enviar Mensaje</button>
    </form>
</div>

Esta implementación MVC en NestJS proporciona una separación clara entre la lógica de negocio (servicios), el control de flujo (controladores) y la presentación (vistas), facilitando el mantenimiento y la escalabilidad de aplicaciones web tradicionales.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Nest

Documentación oficial de Nest
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Nest es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Nest

Explora más contenido relacionado con Nest y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el patrón MVC aplicado en NestJS y su diferencia con APIs REST.
  • Configurar un motor de plantillas Handlebars para renderizar vistas del lado servidor.
  • Organizar la estructura de carpetas para un proyecto MVC en NestJS.
  • Implementar controladores que devuelvan vistas renderizadas con datos dinámicos.
  • Gestionar archivos estáticos y formularios HTML en un entorno MVC con NestJS.