5. Estado atual do sistema
Ao longo dessa aula estruturamos a aplicação seguindo boas práticas de organização de código e responsabilidades bem definidas entre os pacotes. Adotamos conceitos importantes como uso de DTOs para entrada e saída de dados, integração com o banco de dados relacional (MySQL) via Spring Data JPA, paginação e ordenação com Pageable, e mapeamento automático entre entidades e DTOs com ModelMapper. Também discutimos aspectos teóricos como a diferença entre entidades e DTOs, uso de Lombok para redução de boilerplate, princípios de Inversão de Controle e Injeção de Dependência, além da documentação interativa com Swagger/OpenAPI.
Nossa aplicação foi organizada em camadas: config para configurações globais, controller para os endpoints, dto para os objetos de transporte, model para as entidades JPA, repository para o acesso a dados, exception para tratamentos personalizados de erro, e resources para configurações de ambiente. Essa estrutura é nada mais do que uma sequência lógica do que já havíamos adotado nas aulas anteriores.
Nesse estágio atual, portanto, nosso sistema está com a seguinte estrutura de diretórios:
contacts-api/
├── pom.xml
├── src/
│ └── main/
│ ├── java/
│ │ └── br/
│ │ └── ifsp/
│ │ └── contacts/
│ │ ├── config/
│ │ │ └── MapperConfig.java
│ │ ├── controller/
│ │ │ ├── AddressController.java
│ │ │ └── ContactController.java
│ │ ├── dto/
│ │ │ ├── address/
│ │ │ │ ├── AddressRequestDTO.java
│ │ │ │ └── AddressResponseDTO.java
│ │ │ └── contact/
│ │ │ ├── ContactRequestDTO.java
│ │ │ ├── ContactResponseDTO.java
│ │ │ └── ContactPatchDTO.java
│ │ ├── exception/
│ │ │ └── ResourceNotFoundException.java
│ │ │ └── GlobalExceptionHandler.java
│ │ ├── model/
│ │ │ ├── Address.java
│ │ │ └── Contact.java
│ │ ├── repository/
│ │ │ ├── AddressRepository.java
│ │ │ └── ContactRepository.java
│ │ └── ContactsApiApplication.java
│ └── resources/
│ ├── application.properties
│ └── static/
└── target/
Os DTOs, Repositórios e Modelos já foram apresentados anteriormente, bem como o MapperConfig e o ContactsApiApplication. As exceções foram apresentadas na aula passada e não tiveram mudança.
Vamos ver, portanto, como estão os nossos controllers.
ContactController.java
O código-fonte de nosso ContactController ficou da seguinte maneira:
package br.ifsp.contacts.controller;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import br.ifsp.contacts.dto.contact.ContactPatchDTO;
import br.ifsp.contacts.dto.contact.ContactRequestDTO;
import br.ifsp.contacts.dto.contact.ContactResponseDTO;
import br.ifsp.contacts.exception.ResourceNotFoundException;
import br.ifsp.contacts.model.Address;
import br.ifsp.contacts.model.Contact;
import br.ifsp.contacts.repository.ContactRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/contacts")
@Tag(name = "Contatos", description = "Operações relacionadas a contatos")
@Validated
public class ContactController {
@Autowired
private ContactRepository contactRepository;
@Autowired
private ModelMapper modelMapper;
@Operation(summary = "Buscar todos os contatos paginados")
@GetMapping
public Page<ContactResponseDTO> getAllContacts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "nome") String sort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort));
Page<Contact> contacts = contactRepository.findAll(pageable);
return contacts.map(contact -> modelMapper.map(contact, ContactResponseDTO.class));
}
@Operation(summary = "Buscar contato por ID")
@GetMapping("{id}")
public ContactResponseDTO getContactById(@PathVariable Long id) {
Contact contact = contactRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Contato não encontrado: " + id));
return modelMapper.map(contact, ContactResponseDTO.class);
}
@Operation(summary = "Criar novo contato")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ContactResponseDTO createContact(@Valid @RequestBody ContactRequestDTO dto) {
// Mapeia os campos simples
Contact contact = new Contact(dto.getNome(), dto.getEmail(), dto.getTelefone());
// Mapeia os endereços manualmente
var addresses = dto.getAddresses().stream()
.map(addrDto -> {
Address address = new Address();
address.setRua(addrDto.getRua());
address.setCidade(addrDto.getCidade());
address.setEstado(addrDto.getEstado());
address.setCep(addrDto.getCep());
address.setContact(contact);
return address;
}).toList();
contact.setAddresses(addresses);
Contact saved = contactRepository.save(contact);
return modelMapper.map(saved, ContactResponseDTO.class);
}
@Operation(summary = "Atualizar contato por ID")
@PutMapping("/{id}")
public ContactResponseDTO updateContact(@PathVariable Long id, @Valid @RequestBody ContactRequestDTO dto) {
Contact existingContact = contactRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Contato não encontrado: " + id));
modelMapper.map(dto, existingContact);
existingContact.getAddresses().forEach(addr -> addr.setContact(existingContact));
Contact updated = contactRepository.save(existingContact);
return modelMapper.map(updated, ContactResponseDTO.class);
}
@Operation(summary = "Atualização parcial de contato")
@PatchMapping("/{id}")
public ContactResponseDTO updateContactPartial(@PathVariable Long id, @RequestBody ContactPatchDTO dto) {
Contact existingContact = contactRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Contato não encontrado: " + id));
dto.getNome().ifPresent(existingContact::setNome);
dto.getEmail().ifPresent(existingContact::setEmail);
dto.getTelefone().ifPresent(existingContact::setTelefone);
Contact saved = contactRepository.save(existingContact);
return modelMapper.map(saved, ContactResponseDTO.class);
}
@Operation(summary = "Excluir contato")
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteContact(@PathVariable Long id) {
contactRepository.deleteById(id);
}
@Operation(summary = "Buscar contatos pelo nome")
@GetMapping("/search")
public Page<ContactResponseDTO> searchContactsByName(@RequestParam String name, Pageable pageable) {
return contactRepository.findByNomeContainingIgnoreCase(name, pageable)
.map(contact -> modelMapper.map(contact, ContactResponseDTO.class));
}
}
Optamos por manter os import para que vocês não se confundam, pois há annotations com os mesmos nomes em pacotes diferentes quando estamos falando de Page e Pageable.
Já o nosso AddressController ficou como mostrado a seguir.
AddressController.java
package br.ifsp.contacts.controller;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import br.ifsp.contacts.dto.address.AddressRequestDTO;
import br.ifsp.contacts.dto.address.AddressResponseDTO;
import br.ifsp.contacts.exception.ResourceNotFoundException;
import br.ifsp.contacts.model.Address;
import br.ifsp.contacts.model.Contact;
import br.ifsp.contacts.repository.AddressRepository;
import br.ifsp.contacts.repository.ContactRepository;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@RestController
@RequestMapping("/api/addresses")
public class AddressController {
@Autowired
private ContactRepository contactRepository;
@Autowired
private AddressRepository addressRepository;
@Autowired
private ModelMapper modelMapper;
@Operation(summary = "Buscar todos os endereços de um contato")
@GetMapping("/contacts/{contactId}")
public Page<AddressResponseDTO> getAddressesByContact(@PathVariable Long contactId, Pageable pageable) {
return addressRepository.findByContactId(contactId, pageable)
.map(address -> modelMapper.map(address, AddressResponseDTO.class));
}
@Operation(summary = "Criar novo endereço para um contato")
@PostMapping("/contacts/{contactId}")
@ResponseStatus(HttpStatus.CREATED)
public AddressResponseDTO createAddress(@PathVariable Long contactId, @RequestBody @Valid AddressRequestDTO dto) {
Contact contact = contactRepository.findById(contactId)
.orElseThrow(() -> new ResourceNotFoundException("Contato não encontrado: " + contactId));
Address address = modelMapper.map(dto, Address.class);
address.setContact(contact);
Address saved = addressRepository.save(address);
return modelMapper.map(saved, AddressResponseDTO.class);
}
}
📚 Conclusões
Ao longo desta aula, demos mais um passo importante na consolidação das boas práticas no desenvolvimento de APIs RESTful com Spring Boot.
Começamos discutindo a importância do uso de DTOs (Data Transfer Objects), separando os modelos internos da estrutura de dados trafegada pela API. Com isso, ganhamos maior controle sobre o que é exposto ao cliente, evitamos vazamentos de dados sensíveis, reduzimos o acoplamento entre as camadas e preparamos o terreno para uma evolução mais segura da aplicação. Vimos também as vantagens (e limitações) do uso de records e classes com Lombok, além das diferenças entre DTOs de request, response e atualizações parciais com PATCH.
Na sequência, implementamos a paginação e ordenação utilizando os recursos nativos do Spring Data JPA, o que nos permitiu trabalhar com grandes volumes de dados de forma mais performática e organizada. Discutimos a importância de oferecer ao cliente o controle sobre a quantidade e ordenação dos dados retornados, além dos impactos positivos na escalabilidade e usabilidade do sistema.
Depois, configuramos nossa aplicação para utilizar um banco de dados relacional real (MySQL/PostgreSQL) em vez do banco em memória (H2). Essa mudança nos aproximou de um ambiente mais próximo do mundo real, permitindo persistência entre execuções e maior controle sobre os dados. Também discutimos conceitos importantes como ORM, JPA e o papel das migrations no controle de versionamento do banco.
Por fim, integramos a ferramenta Swagger/OpenAPI para documentar nossa API de forma automática, interativa e acessível, promovendo uma comunicação mais clara entre o back-end e seus consumidores. Com apenas algumas anotações e configurações, conseguimos disponibilizar uma interface gráfica que facilita a exploração e o teste da nossa API — um recurso indispensável em qualquer aplicação moderna.
Além dos aspectos técnicos, a aula também reforçou princípios importantes da Engenharia de Software, como a separação de responsabilidades, a validação contextualizada, o desacoplamento entre camadas, o cuidado com overengineering e a importância de decisões técnicas conscientes, baseadas no contexto da aplicação e não apenas em modismos.
Se há uma mensagem principal que queremos reforçar aqui, é esta: não estamos apenas aprendendo frameworks ou ferramentas — estamos aprendendo a construir software com qualidade, clareza e propósito. E isso exige não só domínio técnico, mas também reflexão, criticidade e boas escolhas arquiteturais.
Continue praticando, testando e, acima de tudo, questionando o porquê de cada decisão técnica. Isso é o que transforma uma aplicação funcional em uma aplicação profissional — e um desenvolvedor iniciante em um desenvolvedor consciente.
É importante lembrar que ainda temos muito pela frente. Nas próximas aulas, abordaremos temas como segurança (autenticação e autorização com JWT), versionamento de APIs, e a construção de testes automatizados para garantir a qualidade e a confiabilidade do sistema. Esses tópicos aprofundarão ainda mais nossa aplicação e nosso conhecimento em desenvolvimento de sistemas, de forma geral.
Mas também é fundamental reconhecer o quanto já avançamos. Desde a nossa primeira aula, passamos por conceitos fundamentais de REST, criamos nossos primeiros endpoints, aprendemos a estruturar o projeto com camadas bem definidas, vimos o tratamento de exceções, adotamos boas práticas com DTOs e validações, configuramos a persistência com banco relacional, aplicamos paginação e ordenação, e finalizamos com a documentação interativa da API. Até que não estamos mal, né?! 🎉
É isso, pessoal! Nos vemos na próxima aula! Nessa semana não teremos exercícios para entrega. Apenas verifique ? Avalie os métodos e traga, na próxima aula, ao menos um aprimoramento nos controllers.