Skip to contentPedro Farbo
Lesson 5 / 2550 min

MVC Architecture

MVC Architecture

Let's organize the code with Model-View-Controller pattern adapted for APIs.

MVC for APIs

Routes → Controllers → Services → Models
  • Routes: Define endpoints
  • Controllers: Receive requests and send responses
  • Services: Business logic
  • Models: Database access (Prisma)

Controller

typescript
// src/controllers/product.controller.tsimport { Request, Response } from 'express';import { ProductService } from '../services/product.service'; const productService = new ProductService(); export class ProductController {  async index(req: Request, res: Response) {    const products = await productService.findAll();    return res.json(products);  }   async show(req: Request, res: Response) {    const { id } = req.params;    const product = await productService.findById(id);     if (!product) {      return res.status(404).json({ error: 'Product not found' });    }     return res.json(product);  }   async store(req: Request, res: Response) {    const product = await productService.create(req.body);    return res.status(201).json(product);  }   async update(req: Request, res: Response) {    const { id } = req.params;    const product = await productService.update(id, req.body);    return res.json(product);  }   async destroy(req: Request, res: Response) {    const { id } = req.params;    await productService.delete(id);    return res.status(204).send();  }}

Service

typescript
// src/services/product.service.tsimport { prisma } from '../config/database'; interface CreateProductDTO {  name: string;  price: number;  description?: string;} export class ProductService {  async findAll() {    return prisma.product.findMany();  }   async findById(id: string) {    return prisma.product.findUnique({ where: { id } });  }   async create(data: CreateProductDTO) {    return prisma.product.create({ data });  }   async update(id: string, data: Partial<CreateProductDTO>) {    return prisma.product.update({      where: { id },      data,    });  }   async delete(id: string) {    return prisma.product.delete({ where: { id } });  }}

Routes with Controller

typescript
// src/routes/product.routes.tsimport { Router } from 'express';import { ProductController } from '../controllers/product.controller'; const router = Router();const controller = new ProductController(); router.get('/', (req, res) => controller.index(req, res));router.get('/:id', (req, res) => controller.show(req, res));router.post('/', (req, res) => controller.store(req, res));router.put('/:id', (req, res) => controller.update(req, res));router.delete('/:id', (req, res) => controller.destroy(req, res)); export default router;

AsyncHandler Helper

typescript
// src/utils/async-handler.tsimport { Request, Response, NextFunction } from 'express'; type AsyncFunction = (  req: Request,  res: Response,  next: NextFunction) => Promise<any>; export const asyncHandler = (fn: AsyncFunction) => {  return (req: Request, res: Response, next: NextFunction) => {    Promise.resolve(fn(req, res, next)).catch(next);  };}; // Usagerouter.get('/', asyncHandler(controller.index));

Summary

  • ✅ Controller handles HTTP
  • ✅ Service contains business logic
  • ✅ Clear separation of responsibilities
  • ✅ Easier testing and maintenance

Next lesson: Data Validation with Zod! 🚀

Enjoyed the content? Your contribution helps keep everything online and free!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e