DTO e Mappers
Padrões essenciais para transferência de dados em aplicações Spring Boot - Segurança, Performance e Isolamento
1. O que é DTO?
DTO significa Data Transfer Object. É um padrão de projeto que cria objetos específicos para transferir dados entre camadas de uma aplicação.
Um DTO é uma classe simples que contém apenas getters e setters, sem lógica de negócio. Seu propósito é encapsular e transportar dados.
Diferente de uma Entity (que representa um banco de dados), um DTO pode conter apenas os dados que o cliente precisa receber, evitando exposição desnecessária.
2. Por Que Usar DTO?
Segurança
Não exponha entidades do banco de dados diretamente. Um DTO controla quais dados são enviados ao cliente.
Isolamento de Camadas
A API não fica acoplada com a estrutura do banco de dados. Mudanças na Entity não afetam a resposta da API.
Performance
Envie apenas os dados necessários, reduzindo o tamanho das respostas JSON.
Flexibilidade
Forneça diferentes DTOs para diferentes endpoints sem modificar a Entity.
Validação
Adicione regras de validação específicas para entrada de dados sem afetar a Entity.
3. Exemplo Básico: Entity vs DTO
Entity - Representa o Banco de Dados:
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "usuarios")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nome;
private String email;
@Column(name = "senha_hash")
private String senhaHash; // Nunca deve ser enviado ao cliente!
@Column(name = "data_criacao")
private LocalDateTime dataCriacao;
@Column(name = "cpf")
private String cpf; // Informação sensível
@Column(name = "ativo")
private Boolean ativo;
@OneToMany(mappedBy = "usuario")
private List pedidos; // Relacionamento complexo
}
DTO - Apenas Dados Necessários:
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UsuarioDTO {
private Long id;
private String nome;
private String email;
// Nota: senhaHash NÃO está aqui
// Nota: cpf NÃO está aqui
// Nota: pedidos NÃO está aqui
}
Nunca Exponha:
- Senhas ou hashes de senhas
- Informações sensíveis (CPF, CNPJ, etc)
- Tokens de autenticação
- Relacionamentos complexos desnecessários
- Campos internos da aplicação
4. Request DTO - Receber Dados
Use DTOs para validar e receber dados do cliente. Isso protege sua Entity e permite validação específica.
Exemplo: Criar Novo Usuário
import jakarta.validation.constraints.*;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CriarUsuarioDTO {
@NotBlank(message = "Nome é obrigatório")
@Size(min = 3, max = 100)
private String nome;
@NotBlank(message = "Email é obrigatório")
@Email(message = "Email inválido")
private String email;
@NotBlank(message = "Senha é obrigatória")
@Size(min = 8, message = "Senha deve ter no mínimo 8 caracteres")
private String senha;
}
Usar no Controller:
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
private final UsuarioService service;
public UsuarioController(UsuarioService service) {
this.service = service;
}
@PostMapping
public ResponseEntity criar(
@Valid @RequestBody CriarUsuarioDTO dto) {
// O Spring valida automaticamente
// Se inválido, retorna 400 Bad Request
UsuarioDTO novoUsuario = service.criar(dto);
return ResponseEntity.status(201).body(novoUsuario);
}
}
5. O que é Mapper?
Um Mapper é responsável por converter dados de um tipo para outro. Transforma Entity em DTO e vice-versa.
Responsabilidades do Mapper:
- Entity → DTO (resposta para cliente)
- DTO → Entity (receber dados do cliente)
- Tratamento de valores nulos
- Conversões de tipos quando necessário
- Lógica de transformação de dados
Exemplo Básico - Mapper Manual:
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class UsuarioMapper {
private final PasswordEncoder passwordEncoder;
public UsuarioMapper(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
// Entity para DTO (resposta)
public UsuarioDTO toDTO(Usuario entity) {
if (entity == null) return null;
return new UsuarioDTO(
entity.getId(),
entity.getNome(),
entity.getEmail()
);
}
// DTO para Entity (criar novo)
public Usuario toEntity(CriarUsuarioDTO dto) {
if (dto == null) return null;
Usuario usuario = new Usuario();
usuario.setNome(dto.getNome());
usuario.setEmail(dto.getEmail());
usuario.setSenhaHash(passwordEncoder.encode(dto.getSenha()));
usuario.setDataCriacao(LocalDateTime.now());
usuario.setAtivo(true);
return usuario;
}
// DTO para Entity (atualizar existente)
public void atualizarEntity(AtualizarUsuarioDTO dto, Usuario entity) {
if (dto == null || entity == null) return;
if (dto.getNome() != null) {
entity.setNome(dto.getNome());
}
if (dto.getEmail() != null) {
entity.setEmail(dto.getEmail());
}
}
}
6. Mapper com Listas
Muitas vezes você precisa converter listas inteiras de Entities em DTOs.
Métodos Úteis para Coleções:
import java.util.stream.Collectors;
@Component
public class UsuarioMapper {
// Converter uma lista de Entities para DTOs
public List toDTOList(List entities) {
if (entities == null || entities.isEmpty()) {
return new ArrayList<>();
}
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
// Converter uma página de Entities para DTOs
public Page toDTOPage(Page page) {
return page.map(this::toDTO);
}
// Método auxiliar para DTO
public UsuarioDTO toDTO(Usuario entity) {
if (entity == null) return null;
return new UsuarioDTO(
entity.getId(),
entity.getNome(),
entity.getEmail()
);
}
}
Usando no Service:
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class UsuarioService {
private final UsuarioRepository repository;
private final UsuarioMapper mapper;
public UsuarioService(UsuarioRepository repository, UsuarioMapper mapper) {
this.repository = repository;
this.mapper = mapper;
}
public Page listarTodos(Pageable pageable) {
Page usuarios = repository.findAll(pageable);
return mapper.toDTOPage(usuarios);
}
public List buscarPorNome(String nome) {
List usuarios = repository.findByNomeContaining(nome);
return mapper.toDTOList(usuarios);
}
}
7. MapStruct - Mapper Automático
MapStruct é uma biblioteca que gera Mappers automaticamente em tempo de compilação, sem reflexão.
Configuração no pom.xml:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
<dependencies>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Criar Mapper com MapStruct:
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import java.util.List;
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface UsuarioMapperMapStruct {
// Entity para DTO (simples)
UsuarioDTO toDTO(Usuario entity);
// Lista de Entities para DTOs
List toDTOList(List entities);
// DTO para Entity (com mapeamento customizado)
@Mapping(target = "senhaHash",
expression = "java(org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(dto.getSenha()))")
@Mapping(target = "dataCriacao",
expression = "java(java.time.LocalDateTime.now())")
@Mapping(target = "ativo", constant = "true")
Usuario toEntity(CriarUsuarioDTO dto);
// Atualizar Entity existente
@Mapping(target = "id", ignore = true)
@Mapping(target = "senhaHash", ignore = true)
@Mapping(target = "dataCriacao", ignore = true)
void atualizarEntity(AtualizarUsuarioDTO dto, @MappingTarget Usuario entity);
}
Vantagens do MapStruct:
- Gera código em tempo de compilação (sem reflexão)
- Muito mais rápido que mapeamento manual
- Type-safe (verificado em tempo de compilação)
- Permite customizações com @Mapping
- Integração natural com Spring
- Zero overhead em runtime
8. Exemplo Completo: API de Usuários
1. DTOs:
// Response DTO
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UsuarioDTO {
private Long id;
private String nome;
private String email;
}
// Request DTO - Criar
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CriarUsuarioDTO {
@NotBlank
@Size(min = 3, max = 100)
private String nome;
@NotBlank
@Email
private String email;
@NotBlank
@Size(min = 8)
private String senha;
}
// Request DTO - Atualizar
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AtualizarUsuarioDTO {
@Size(min = 3, max = 100)
private String nome;
@Email
private String email;
}
2. Mapper:
@Mapper(componentModel = "spring")
public interface UsuarioMapper {
UsuarioDTO toDTO(Usuario entity);
List toDTOList(List entities);
@Mapping(target = "senhaHash", ignore = true)
Usuario toEntity(CriarUsuarioDTO dto);
@Mapping(target = "id", ignore = true)
@Mapping(target = "senhaHash", ignore = true)
void update(AtualizarUsuarioDTO dto, @MappingTarget Usuario entity);
}
3. Service:
@Service
public class UsuarioService {
private final UsuarioRepository repository;
private final UsuarioMapper mapper;
private final PasswordEncoder passwordEncoder;
public UsuarioService(UsuarioRepository repository,
UsuarioMapper mapper,
PasswordEncoder passwordEncoder) {
this.repository = repository;
this.mapper = mapper;
this.passwordEncoder = passwordEncoder;
}
public UsuarioDTO criar(CriarUsuarioDTO dto) {
Usuario entity = mapper.toEntity(dto);
entity.setSenhaHash(passwordEncoder.encode(dto.getSenha()));
Usuario salvo = repository.save(entity);
return mapper.toDTO(salvo);
}
public UsuarioDTO buscarPorId(Long id) {
Usuario usuario = repository.findById(id)
.orElseThrow(() -> new NotFoundException("Usuário não encontrado"));
return mapper.toDTO(usuario);
}
public Page listarTodos(Pageable pageable) {
Page page = repository.findAll(pageable);
return page.map(mapper::toDTO);
}
public UsuarioDTO atualizar(Long id, AtualizarUsuarioDTO dto) {
Usuario usuario = repository.findById(id)
.orElseThrow(() -> new NotFoundException("Usuário não encontrado"));
mapper.update(dto, usuario);
Usuario atualizado = repository.save(usuario);
return mapper.toDTO(atualizado);
}
public void deletar(Long id) {
if (!repository.existsById(id)) {
throw new NotFoundException("Usuário não encontrado");
}
repository.deleteById(id);
}
}
4. Controller:
@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
private final UsuarioService service;
public UsuarioController(UsuarioService service) {
this.service = service;
}
@PostMapping
public ResponseEntity criar(
@Valid @RequestBody CriarUsuarioDTO dto) {
UsuarioDTO resultado = service.criar(dto);
return ResponseEntity.status(201).body(resultado);
}
@GetMapping("/{id}")
public ResponseEntity buscarPorId(@PathVariable Long id) {
UsuarioDTO usuario = service.buscarPorId(id);
return ResponseEntity.ok(usuario);
}
@GetMapping
public ResponseEntity> listarTodos(Pageable pageable) {
Page usuarios = service.listarTodos(pageable);
return ResponseEntity.ok(usuarios);
}
@PutMapping("/{id}")
public ResponseEntity atualizar(
@PathVariable Long id,
@Valid @RequestBody AtualizarUsuarioDTO dto) {
UsuarioDTO atualizado = service.atualizar(id, dto);
return ResponseEntity.ok(atualizado);
}
@DeleteMapping("/{id}")
public ResponseEntity deletar(@PathVariable Long id) {
service.deletar(id);
return ResponseEntity.noContent().build();
}
}
9. Casos de Uso Comuns
Caso 1: DTO Parcial - Listagem
Às vezes você quer menos campos na listagem que na visualização individual:
// DTO com todos os dados
@Getter
@Setter
public class UsuarioDTO {
private Long id;
private String nome;
private String email;
private LocalDateTime dataCriacao;
}
// DTO simplificado para listagem
@Getter
@Setter
public class UsuarioResumoDTO {
private Long id;
private String nome;
private String email;
}
Caso 2: DTO com Relacionamentos
Incluir dados relacionados sem expor a Entity completa:
@Getter
@Setter
public class PedidoDTO {
private Long id;
private LocalDateTime data;
private BigDecimal total;
// Dados do usuário relacionado (simplificado)
private UsuarioResumoDTO usuario;
// Lista de itens do pedido
private List itens;
}
@Getter
@Setter
public class ItemPedidoDTO {
private Long id;
private String nomeProduto;
private Integer quantidade;
private BigDecimal precoUnitario;
}
Caso 3: DTO com Validação Customizada
@Getter
@Setter
public class CriarProdutoDTO {
@NotBlank
@Size(min = 3, max = 100)
private String nome;
@NotBlank
private String descricao;
@NotNull
@DecimalMin("0.01")
private BigDecimal preco;
@NotNull
@Min(0)
private Integer estoque;
@Email
private String emailFornecedor;
}
10. Boas Práticas com DTO e Mappers
✓ FAÇA:
- Separe Request e Response DTOs
- Use validação Bean Validation (@Valid)
- Nunca envie senhas ou dados sensíveis
- Use Mappers para conversão consistente
- Crie DTOs específicos para cada caso de uso
- Documente quais campos são opcionais
- Use tipos específicos (LocalDateTime, BigDecimal, etc)
- Implemente equals() e hashCode() se necessário
✗ EVITE:
- Usar Entity diretamente como response
- Expor detalhes internos do banco de dados
- Mappers sem estrutura ou padrão
- DTOs com lógica de negócio
- DTOs muito genéricos que servem para tudo
- Enviar dados desnecessários (performance)
- Mappers sem testes
- Esquecer validação em Request DTOs
11. Erros Comuns
❌ Erro 1: Expor a Entity Diretamente
// ERRADO - Expõe tudo, inclusive senha!
@GetMapping("/{id}")
public ResponseEntity buscar(@PathVariable Long id) {
Usuario usuario = repository.findById(id).orElseThrow();
return ResponseEntity.ok(usuario);
}
// CORRETO - Retorna DTO
@GetMapping("/{id}")
public ResponseEntity buscar(@PathVariable Long id) {
Usuario usuario = repository.findById(id).orElseThrow();
return ResponseEntity.ok(mapper.toDTO(usuario));
}
❌ Erro 2: DTO com Lógica de Negócio
// ERRADO - DTO com lógica
public class UsuarioDTO {
private String nome;
public String getNomeFormatado() {
return nome.toUpperCase();
}
}
// CORRETO - DTO simples, lógica no Service
public class UsuarioDTO {
private String nome;
}
❌ Erro 3: Mapper sem Validação
// ERRADO - Sem tratamento de nulo
public UsuarioDTO toDTO(Usuario entity) {
return new UsuarioDTO(
entity.getId(), // Pode dar NullPointerException
entity.getNome(),
entity.getEmail()
);
}
// CORRETO - Valida antes
public UsuarioDTO toDTO(Usuario entity) {
if (entity == null) return null;
return new UsuarioDTO(
entity.getId(),
entity.getNome(),
entity.getEmail()
);
}
12. Estrutura de Projeto Recomendada
src/main/java/com/example/projeto/
├── controller/
│ └── UsuarioController.java
├── service/
│ └── UsuarioService.java
├── repository/
│ └── UsuarioRepository.java
├── mapper/
│ ├── UsuarioMapper.java
│ └── PedidoMapper.java
├── dto/
│ ├── request/
│ │ ├── CriarUsuarioDTO.java
│ │ └── AtualizarUsuarioDTO.java
│ └── response/
│ ├── UsuarioDTO.java
│ └── UsuarioResumoDTO.java
├── entity/
│ ├── Usuario.java
│ └── Pedido.java
└── config/
└── MapperConfig.java
13. Resumo - DTO vs Mapper
DTO (Data Transfer Object)
- Classe simples com getters/setters
- Transfere dados entre camadas
- Sem lógica de negócio
- Protege a Entity
- Pode ter validação específica
Mapper
- Converte Entity em DTO e vice-versa
- Centraliza lógica de transformação
- Pode ser manual ou automático (MapStruct)
- Trata valores nulos e conversões
- Facilita testes e manutenção
Conclusão
DTO e Mappers são padrões essenciais em APIs Spring Boot modernas.
Eles garantem:
- ✓ Segurança - Dados sensíveis protegidos
- ✓ Isolamento - Entity independente da API
- ✓ Performance - Apenas dados necessários
- ✓ Flexibilidade - Múltiplos DTOs para casos diferentes
- ✓ Manutenibilidade - Código organizado e consistente
Use sempre DTOs em suas APIs REST e padronize as conversões com Mappers! 🚀