Pular para o conteúdoPedro Farbo
Lição 5 / 2545 min

Arquitetura MVC

Arquitetura MVC

À medida que sua aplicação cresce, manter todo o código em um único arquivo se torna insustentável. Nesta aula, vamos aprender a organizar o código usando o padrão MVC (Model-View-Controller) adaptado para APIs.

O que é MVC?

MVC separa a aplicação em três camadas:

┌─────────────────────────────────────────────────────────────┐
│                         REQUEST                              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                       CONTROLLER                             │
│            (Recebe request, valida, responde)               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        SERVICE                               │
│               (Lógica de negócio)                           │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      REPOSITORY                              │
│               (Acesso a dados)                              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        MODEL                                 │
│               (Estrutura dos dados)                         │
└─────────────────────────────────────────────────────────────┘

Responsabilidades

CamadaResponsabilidade
ControllerReceber requisição, validar entrada, chamar service, enviar resposta
ServiceImplementar regras de negócio, orquestrar operações
RepositoryAbstrair acesso ao banco de dados
ModelDefinir estrutura e tipos dos dados

Estrutura de Pastas

src/
├── controllers/
│   └── user.controller.ts
├── services/
│   └── user.service.ts
├── repositories/
│   └── user.repository.ts
├── models/
│   └── user.model.ts
├── routes/
│   └── user.routes.ts
├── middlewares/
├── utils/
├── types/
├── app.ts
└── server.ts

Implementando MVC

Vamos implementar um módulo de usuários completo:

1. Model (Tipos)

typescript
// src/models/user.model.tsexport interface User {  id: string;  name: string;  email: string;  password: string;  role: 'user' | 'admin';  createdAt: Date;  updatedAt: Date;} export interface CreateUserDTO {  name: string;  email: string;  password: string;  role?: 'user' | 'admin';} export interface UpdateUserDTO {  name?: string;  email?: string;  password?: string;  role?: 'user' | 'admin';} // Usuário sem a senha (para respostas)export type UserResponse = Omit<User, 'password'>;

2. Repository

typescript
// src/repositories/user.repository.tsimport { User, CreateUserDTO, UpdateUserDTO } from '../models/user.model';import { randomUUID } from 'crypto'; // Simulando banco de dados em memória (será substituído por Prisma)const users: User[] = []; export class UserRepository {  async findAll(): Promise<User[]> {    return users;  }   async findById(id: string): Promise<User | null> {    return users.find(user => user.id === id) || null;  }   async findByEmail(email: string): Promise<User | null> {    return users.find(user => user.email === email) || null;  }   async create(data: CreateUserDTO): Promise<User> {    const user: User = {      id: randomUUID(),      ...data,      role: data.role || 'user',      createdAt: new Date(),      updatedAt: new Date(),    };     users.push(user);    return user;  }   async update(id: string, data: UpdateUserDTO): Promise<User | null> {    const index = users.findIndex(user => user.id === id);     if (index === -1) return null;     users[index] = {      ...users[index],      ...data,      updatedAt: new Date(),    };     return users[index];  }   async delete(id: string): Promise<boolean> {    const index = users.findIndex(user => user.id === id);     if (index === -1) return false;     users.splice(index, 1);    return true;  }}

3. Service

typescript
// src/services/user.service.tsimport { hash } from 'bcrypt';import { UserRepository } from '../repositories/user.repository';import { CreateUserDTO, UpdateUserDTO, User, UserResponse } from '../models/user.model';import { AppError } from '../middlewares/errorHandler.middleware'; export class UserService {  constructor(private userRepository: UserRepository) {}   private toUserResponse(user: User): UserResponse {    const { password, ...userWithoutPassword } = user;    return userWithoutPassword;  }   async findAll(): Promise<UserResponse[]> {    const users = await this.userRepository.findAll();    return users.map(this.toUserResponse);  }   async findById(id: string): Promise<UserResponse> {    const user = await this.userRepository.findById(id);     if (!user) {      throw new AppError(404, 'Usuário não encontrado');    }     return this.toUserResponse(user);  }   async create(data: CreateUserDTO): Promise<UserResponse> {    // Verificar se email já existe    const existingUser = await this.userRepository.findByEmail(data.email);     if (existingUser) {      throw new AppError(409, 'Email já cadastrado');    }     // Hash da senha    const hashedPassword = await hash(data.password, 10);     const user = await this.userRepository.create({      ...data,      password: hashedPassword,    });     return this.toUserResponse(user);  }   async update(id: string, data: UpdateUserDTO): Promise<UserResponse> {    // Verificar se usuário existe    const existingUser = await this.userRepository.findById(id);     if (!existingUser) {      throw new AppError(404, 'Usuário não encontrado');    }     // Se está atualizando email, verificar duplicidade    if (data.email && data.email !== existingUser.email) {      const userWithEmail = await this.userRepository.findByEmail(data.email);       if (userWithEmail) {        throw new AppError(409, 'Email já está em uso');      }    }     // Hash da senha se foi alterada    if (data.password) {      data.password = await hash(data.password, 10);    }     const updatedUser = await this.userRepository.update(id, data);     return this.toUserResponse(updatedUser!);  }   async delete(id: string): Promise<void> {    const deleted = await this.userRepository.delete(id);     if (!deleted) {      throw new AppError(404, 'Usuário não encontrado');    }  }}

4. Controller

typescript
// src/controllers/user.controller.tsimport { Request, Response } from 'express';import { UserService } from '../services/user.service';import { CreateUserDTO, UpdateUserDTO } from '../models/user.model'; export class UserController {  constructor(private userService: UserService) {}   findAll = async (req: Request, res: Response): Promise<void> => {    const users = await this.userService.findAll();     res.json({      success: true,      data: users,    });  };   findById = async (req: Request, res: Response): Promise<void> => {    const { id } = req.params;    const user = await this.userService.findById(id);     res.json({      success: true,      data: user,    });  };   create = async (req: Request, res: Response): Promise<void> => {    const data: CreateUserDTO = req.body;    const user = await this.userService.create(data);     res.status(201).json({      success: true,      data: user,      message: 'Usuário criado com sucesso',    });  };   update = async (req: Request, res: Response): Promise<void> => {    const { id } = req.params;    const data: UpdateUserDTO = req.body;    const user = await this.userService.update(id, data);     res.json({      success: true,      data: user,      message: 'Usuário atualizado com sucesso',    });  };   delete = async (req: Request, res: Response): Promise<void> => {    const { id } = req.params;    await this.userService.delete(id);     res.status(204).send();  };}

5. Routes

typescript
// src/routes/user.routes.tsimport { Router } from 'express';import { UserController } from '../controllers/user.controller';import { UserService } from '../services/user.service';import { UserRepository } from '../repositories/user.repository';import { asyncHandler } from '../utils/asyncHandler'; const router = Router(); // Injeção de dependências manualconst userRepository = new UserRepository();const userService = new UserService(userRepository);const userController = new UserController(userService); router.get('/', asyncHandler(userController.findAll));router.get('/:id', asyncHandler(userController.findById));router.post('/', asyncHandler(userController.create));router.put('/:id', asyncHandler(userController.update));router.delete('/:id', asyncHandler(userController.delete)); export default router;

Injeção de Dependências

Para facilitar testes e manutenção, use injeção de dependências:

typescript
// src/container.tsimport { UserRepository } from './repositories/user.repository';import { UserService } from './services/user.service';import { UserController } from './controllers/user.controller'; // Instâncias compartilhadasconst userRepository = new UserRepository();const userService = new UserService(userRepository);const userController = new UserController(userService); export const container = {  userRepository,  userService,  userController,};

Por que usar essa arquitetura?

1. Testabilidade

typescript
// Fácil de mockar dependências em testesdescribe('UserService', () => {  let userService: UserService;  let mockRepository: jest.Mocked<UserRepository>;   beforeEach(() => {    mockRepository = {      findAll: jest.fn(),      findById: jest.fn(),      findByEmail: jest.fn(),      create: jest.fn(),      update: jest.fn(),      delete: jest.fn(),    };     userService = new UserService(mockRepository);  });   it('should throw error when user not found', async () => {    mockRepository.findById.mockResolvedValue(null);     await expect(userService.findById('invalid-id'))      .rejects.toThrow('Usuário não encontrado');  });});

2. Manutenibilidade

  • Mudanças no banco afetam apenas o Repository
  • Novas regras de negócio vão para o Service
  • Controllers permanecem limpos e focados

3. Reusabilidade

typescript
// Mesmo service pode ser usado em diferentes controllersclass AdminController {  constructor(private userService: UserService) {}   async promoteToAdmin(req: Request, res: Response) {    await this.userService.update(req.params.id, { role: 'admin' });    res.json({ message: 'Usuário promovido' });  }}

Resumo

Nesta aula aprendemos:

  • ✅ O padrão MVC adaptado para APIs
  • ✅ Responsabilidades de cada camada
  • ✅ Implementar Model, Repository, Service, Controller
  • ✅ Organizar rotas com injeção de dependências
  • ✅ Benefícios da arquitetura em camadas

Na próxima aula, vamos aprender Validação de Dados com Zod! ✅

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

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