O padrão Model-Controller-Service é, na minha opinião, a melhor arquitetura padrão para backends em TypeScript. Não é glamouroso, mas produz de forma consistente bases de código fáceis de navegar, estender e repassar para outros engenheiros.

A Importância de uma Arquitetura Backend Sólida

Uma arquitetura backend bem projetada estabelece a base para uma aplicação escalável e de fácil manutenção. Sem ela, os projetos acumulam dívida técnica rapidamente: a lógica acaba espalhada, os testes ficam frágeis e cada novo recurso arrisca quebrar algo não relacionado.

O TypeScript adiciona um sistema de tipagem forte ao JavaScript, permitindo que você detecte erros em tempo de compilação e escreva código mais previsível. Mas os tipos por si só não salvam você de uma base de código mal organizada.

Padrões de Arquitetura Comuns

Vários padrões são comuns no desenvolvimento backend:

  • Model-View-Controller (MVC) separa as aplicações em Models, Views e Controllers.
  • Model-View-ViewModel (MVVM) foca no data binding entre View e ViewModel.
  • Microsserviços dividem as aplicações em serviços fracamente acoplados.

Cada um tem seu lugar. O padrão Model-Controller-Service oferece um recorte mais focado para backends em TypeScript onde não há camada de view renderizada no servidor.

Entendendo o Padrão Model-Controller-Service

O padrão divide a lógica da aplicação em três componentes.

1. Models

Os Models representam as estruturas de dados da sua aplicação. Eles definem a forma dos dados e frequentemente mapeiam diretamente para schemas de banco de dados. Em TypeScript, você os define como interfaces ou classes.

// models/User.ts

export interface User {
  id: number;
  name: string;
  email: string;
}

2. Controllers

Os Controllers tratam as requisições HTTP recebidas e retornam respostas ao cliente. Eles são o ponto de entrada de cada endpoint e coordenam a comunicação entre o cliente e a camada de serviço.

// controllers/UserController.ts

import { Request, Response } from 'express';
import { UserService } from '../services/UserService';

export class UserController {
  private userService = new UserService();

  public async getUser(req: Request, res: Response): Promise<void> {
    const userId = req.params.id;
    const user = await this.userService.getUserById(userId);
    res.json(user);
  }
}

3. Services

Os Services contêm a lógica de negócio e gerenciam a comunicação com o banco de dados ou APIs externas. Eles realizam a recuperação, manipulação e validação de dados.

// services/UserService.ts

import { User } from '../models/User';

export class UserService {
  public async getUserById(id: number): Promise<User> {
    // Logic to retrieve user from the database
  }
}

Vantagens do Padrão Model-Controller-Service em TypeScript

Separação de Responsabilidades

Cada componente tem uma responsabilidade distinta. Os Controllers não sabem como os dados são buscados; os Services não sabem qual código de status HTTP retornar. Essa separação facilita o raciocínio sobre cada parte e a alteração independente de cada uma.

Escalabilidade

Uma arquitetura modular torna a adição de novos recursos gerenciável. Dois engenheiros podem trabalhar em endpoints diferentes simultaneamente sem interferir um no trabalho do outro.

Manutenibilidade

A estrutura clara reduz o tempo necessário para corrigir bugs ou entender o fluxo de dados. Quando algo quebra, você sabe em qual camada olhar primeiro.

Testabilidade

Services e Controllers isolados permitem que você escreva testes unitários focados sem precisar iniciar a aplicação inteira. Testar um Service significa testar a lógica de negócio diretamente, sem overhead de HTTP.

Implementando o Padrão MCS: Boas Práticas

Aproveite o Sistema de Tipos do TypeScript

Defina models usando interfaces e use anotações de tipo em todo o código. Isso detecta incompatibilidades entre o que seu Service retorna e o que seu Controller espera.

// models/Product.ts

export interface Product {
  id: number;
  name: string;
  price: number;
}

Use Injeção de Dependência

Passe Services para os Controllers em vez de instanciá-los internamente. Isso desacopla os componentes e torna os testes unitários simples.

// controllers/ProductController.ts

import { ProductService } from '../services/ProductService';

export class ProductController {
  constructor(private productService: ProductService) {}

  // Controller methods
}

Organize a Estrutura de Arquivos

Mantenha uma estrutura de diretórios consistente que espelhe o padrão. Qualquer pessoa nova no projeto deve conseguir encontrar qualquer arquivo em menos de dez segundos.

src/
├── controllers/
│   └── [Name]Controller.ts
├── models/
│   └── [Name].ts
├── services/
│   └── [Name]Service.ts

Tratamento de Erros e Logging

Centralize o tratamento de erros em middleware em vez de duplicar try-catch em cada Controller. O Express torna isso simples.

// middleware/errorHandler.ts

export function errorHandler(err, req, res, next) {
  // Log error details
  res.status(500).json({ error: 'Internal Server Error' });
}

Aplique os Princípios SOLID

O padrão MCS combina naturalmente com os princípios SOLID, especialmente responsabilidade única e inversão de dependência. Segui-los mantém cada camada limpa à medida que a aplicação cresce.

Integrando o Padrão MCS com Express.js

O Express se combina bem com TypeScript e o padrão MCS.

Configurando o Express com TypeScript

Inicialize um novo projeto Node.js e instale as dependências necessárias:

npm init -y
npm install express
npm install -D typescript ts-node @types/express

Configure seu tsconfig.json para a compilação TypeScript.

Criando uma Aplicação Express

// app.ts

import express from 'express';
import { UserController } from './controllers/UserController';

const app = express();
const userController = new UserController();

app.get('/users/:id', userController.getUser.bind(userController));

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Middleware e Roteamento

Use middleware para parsear corpos de requisição e lidar com autenticação. Defina rotas que mapeiam para métodos do Controller.

// routes/userRoutes.ts

import { Router } from 'express';
import { UserController } from '../controllers/UserController';

const router = Router();
const userController = new UserController();

router.get('/:id', userController.getUser.bind(userController));

export default router;

Aproveitando Recursos Avançados do TypeScript

O TypeScript possui vários recursos que se encaixam bem nessa arquitetura.

Generics

Use generics para escrever Services base reutilizáveis em vez de duplicar o boilerplate de CRUD.

// services/BaseService.ts

export class BaseService<T> {
  // Generic methods for CRUD operations
}

Enums e Tipos Avançados

Defina enums para estados específicos em vez de espalhar strings mágicas pelo seu código.

// models/OrderStatus.ts

export enum OrderStatus {
  Pending,
  Shipped,
  Delivered,
  Cancelled,
}

Decorators

Use decorators para metadados e anotações, especialmente ao usar bibliotecas como TypeORM.

// models/Customer.ts

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

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

  @Column()
  name: string;
}

Otimizando para Escalabilidade e Performance

Operações Assíncronas

Use async/await em todos os seus Services para manter as operações de I/O sem bloqueio.

public async getAllUsers(): Promise<User[]> {
  const users = await this.userRepository.findAll();
  return users;
}

Estratégias de Cache

Adicione cache na camada de Service para reduzir a carga no banco de dados. O Redis funciona bem aqui e é fácil de integrar sem tocar nos seus Controllers.

Balanceamento de Carga e Escalabilidade Horizontal

Mantenha os Services stateless. Services stateless escalam horizontalmente sem overhead de coordenação, e um load balancer pode distribuir o tráfego entre instâncias sem precisar de sessões fixas.

Conclusão

O padrão Model-Controller-Service oferece uma maneira clara e consistente de organizar backends TypeScript. A separação de responsabilidades é real e prática: com o tempo você perceberá que os bugs ficam mais fáceis de localizar, o onboarding de novos engenheiros se torna mais rápido e a adição de recursos não exige tocar em código não relacionado. Esse é o retorno real de adotar essa estrutura.