README
Atualizado
Atualizado
Aprenderemos funcionalidades que a nossa aplicação irá utilizar, por exemplo:
CRUD - Seria a tela de cadastro de produtos, onde podemos:
Inserir;
Atualizar;
Deletar;
Consultar.
As funcionalidades do CRUD serão por meio da web (API via web).
Todas as premissas e o sumário com o que deve ser feito está no "Documento de Requesitos DSCommerce.pdf". Como é algo específico do curso, não colocarei o link, mas você pode adquirir no site devsuperior.
Primeiramente, precisamos saber o que é uma API. Uma API é um conjunto de funcionalidades que são expostas por uma aplicação/módulo.
Pode ser considerada também como um contrato entre um provedor e um consumidor de funcionalidades.
API disponibilizada via web. Suas funcionalidades são acessadas via endpoints web (o endereço que colocamos no navegador com aquela "/"), exemplo: host, porta, rota, parâmetros, corpo(payload), de cabeçalhos, usando um protocolo HTTP.
É uma API Web que está e conformidade com as restrições do padrão REST.
Padrão REST - Regras
Cliente/servidor com HTTP;
Uma aplicação frontend pode acessar a API via HTTP, por exemplo.
Comunicação stateless;
Cada requisição será independente e isolada.
Cache;
Armazenar dados em memória para melhor aproveitamento.
Interface uniforme, formato padronizado;
Rotas (especificações de como acessar a API) padronizado.
Sistema em camadas;
Código sob demanda (opcional)
As funcionalidades e informações de um sistema web são organizadas na forma de RECURSOS.
Cada recurso corresponde a alguma entidade do nosso negócio (ou conjunto de informações).
URL - Universal Resource Locator
A URL deve acessar os recursos pelo nome:
GET:host:port/products - (obtém os produtos)
GET:host:port/products?page=3 - (obter produtos página 3)
GET:host:port/products/1 - (obter produto id 1)
GET:host:port/products1/categories - (obter categorias do produto id 1)
Quando definimos a nossa rota, ela precisa ter um formato padronizado.
A ação que desejamos fazer deve ser expressa pelo verbo HTTP e não por sua rota.
ERRADO: Não é correto colocar o verbo da ação (insert) na rota
GET:host:port/insertProduct
GET:host:port/listProduct
CERTO: Usar os verbos HTPP (post) pra inserir, (get) para obter, etc.
POST:host:port/products
GET:host:port/products
GET - obter recurso;
POST - criar novo recurso;
PUT - salvar recurso de forma idempotente;
DELETE - deletar recurso
❗Operação idempotente = não causa novos efeitos se executada mais de uma vez.
Respostas de informação (100-199);
Respostas de sucesso (200-299);
Redirecionamentos (300-399);
Erros do cliente (400-499);
Erros do Servidor (500-599).
Organizando a aplicação em camadas com responsabilidades definidas (uma forma de reestruturar o sistema), deixando o mesmo em um estado de fácil manutenção.
Consiste em organizar os COMPONENTES do sistema em partes denominadas camadas;
Componentes entenda por SERVICE, ProductService, ClientService, objetos que fazem funções/operações. Entidades como Produto, Pedido não entram nesse quesito.
Cada camada possui uma responsabilidade específica;
Na hora de realizar alguma manutenção, trocar qualquer coisa já sabemos onde aquilo estará pois fizemos a devidade separação das camadas;
Componentes de uma camada só podem depender de componentes da mesma camada, ou da camada mais abaixo, veja:
Controladores conversam com a camada de Service através de DTO.
Por sua vez, camada de Service conversa com a camada de acesso a dados através de Entidades (Product, Category, User).
Portanto, resumindo a imagem:
Na camada de acesso a dados as entidades estarão devidamente mapeadas para conversar com a camada de Service.
Para a camada de serviço conversar com a de Controle, as entidades mapeadas serão convertidas em DTO.
TRANSAÇÃO
Tudo que for transação, ou seja, acesso a banco de dados sera resolvido na camada de serviço com acesso a dados.
Controlador: responder interações do usuário.
No caso de uma API REST, essas "interações" são as REQUISIÇÕES.
Service: realizar operações de negócio:
Um método da camada Service, deve ter um SIGNIFICADO relacionado ao negócio. Podendo executar várias operações. Exemplo: registrarPedido, somente este método terá dentro dele: [verificar estoque, salvar pedido, baixar estoque, enviar email].
Repository: realizará operações "individuais" de acesso ao banco de dados.
Métodos findAll, findById, consulta SQL, inserção/atualização/deleção de registros, etc...
Criação do pacote controllers
Criar uma classe ProductController
Nela, será onde disponibilazaremos os recursos (GET, POST...), implementando-os.
Configuração da Classe
Veja tudo que foi feito: ProductController
A partir disso, ao rodar o programa, poderemos utilizar o Postman para fazer as requisições :)
Seria possível buscar um produto do banco de dados e imprimir o seu nome em um método Get? Vejamos!
Sabemos que o Repository é responsável por acessos os dados, então criaremos um!
Criação do pacote repositories
Criar uma interface ProductRepository
Veja tudo que foi feito: ProductRepository
Depois disso, o Repository precisa ser injetado na classe de Controle!
A partir disso, podemos usá-lo dentro dos métodos HTTP, possuindo um mundo de métodos disponíveis:
Como a nossa proposta conforme dito acima é procurar um produto específico, utilizaremos o findById.
Ao rodar o código, poderemos buscar no Postman, veja:
Beleza, perfeito e funcionou. Mas tá errado, correto? O ideal conforme vimos lá em cima é o Controller depender de um Service. Neste cenário acima, ele está dependendo de um Repository.
Outra coisa, se você observar no método, nós estamos passando a id desejada "1L", como parâmetro.
E por fim, o nosso endpoint não retornará somente uma String e sim o Objeto todo do Product.
Vamos arruamar isso. 👇
Conforme destacado acima, pontuamos que possuimos diversas coisas a serem melhoradas, vamos lá!
DTO - Data Transfer Object é um objeto simples para transferirmos dados.
Ele não é gerenciado por uma lib de ORM (JPA) / acesso a dados.
Além disso, pode conter outros DTO's aninhados.
❗NUNCA ANINHE UMA ENTITY DENTRO DE UM DTO
Diversos motivos, veja:
Projeção de Dados (projetar somente os dados que você precisa). O Product tem diversos atributos mas você pode, por exemplo, querer uma busca de dados mais simples com dados básicos (id e nome), e isso pode ser feito. Nós não precisamos expor a senha de um User ao criar um DTO.
Segurança
Economia de Tráfego
Flexibilidade: permite que a API trafegue mais de uma representação dos dados. Ou seja, uma entidade pode ter outros DTOS.
Para preencher um combobox: {id: number, nome: string}
Para um relatório detalhado: {id: number, nome: string, salario: number, email: string, telefones: string[] }
Separação de responsabilidades
Service e repository: transação e monitoramento ORM
Controller: tráfego simples de dados
Na prática:
Veja o DTO criado aqui.
Criaremos um pacote chamado dto
Dentro dele, um ProductDto do tipo Record
Pegaremos os dados básicos que iremos utilizar
Criar construtor (com e sem argumentos)
Gerar Getters. Setters não precisa, pois não faz sentido alterarmos esses dados.
Bom, como sabemos, o Controller não pode depender do Repository. Vamos organizar as camadas.
Veja a classe criada aqui.
Criar pacote services
Criar classe ProductService
Passar anotação @Service
Injetar Repository para aí sim o service depender da camada de acesso a dados
Agora sim implementaremos a busca no banco da dados, veja o método:
Agora no Controller, tiramos o Repository e injetamos o Service :)
Método do Controller:
Pode ser feita a cópia manual (set / construtor), passando argumento por argumento no construtor.
Ou usar alguam lib que copiará atributos de mesmo nome de um objeto para outro, como o ModelMapper.
Create, salvando um novo registro.
Retrieve, recuperando todos os registros (paginados) ou somente um (por id)
Update, atualizar dado um id.
Delete, deletar dado um id.
Primeiro, faremos um método para buscar todos os itens, veja:
No Service:
No Controller:
É muito simples. Dentro do método do Controller, podemnos passar um parametro chamado "Pageable".
Passaremos esse pageable dentro do findAll, veja:
Mas o nosso service também receberá esse pageable!
Por padrão, o Pageable retorna 20 elementos
Resultado de página por tamanho
E se quiséssemos 12 resultados ao invés de 20? Colocaremos um "QueryParam" no postman, veja:
Órdem alfabética
No service:
No Controller
No nosso Controller, ao invés de retornarmos só DTOS ou Pages, retornaremos também ResponseEntity, veja:
Você pode conferir outros exemplos das alterações com ResponseEntity aqui
Em método de criação (insert) o ".CREATED", precisa receber uma URI como parametro, então fazemos dessa forma:
Desta maneira, ao realizar a inserção de dados no postman e consultar o item inserido, ele terá uma URI personalizada, veja:
No postman, será um PUT. Na URI passaremos o id a ser alterado juntamente com o corpo dos itens a serem alterados, veja:
A diferença para o insert é aqui precisamos instanciar o objeto como referência (getReferenceById).
Service:
Controller:
Service:
Controller:
Primeira coisa é criar um subpacote exceptions na camada que vamos trabalhar. O service pode ter, controller também, etc.
Trataremos as exceções utilizando ControllerAdvice.
Devolveremos algum código de erro na faixa do 400, veja:
Para tratar a exceção nos métodos, usaremos um try-catch. No entanto, alguns métodos (como findById), o Optional tem um método que já lança uma exceção, chamado "orElseThrow()":
Service:
Ok, conforme visto acima, nós implementamos uma exceção customizada. Agora, precisamos tratar esse erro, capturando essa exceção, devolvendo uma resposta customizada no Postman.
Criaremos no pacote DTO uma classe chamada CustomError, contendo todos os atributos que é retornando em JSON no postman.
Veja a classe CustomError
Agora, usaremos a classe ControllerAdvice! Essa classe, podemos definir tratamentos globais para exceções específicas, sem precisar ficar usando vários try-catch em diversas partes do código.
Criamos um subpacote em controllers chamado handlers.
Criaremos a classe ControllerExceptionHandler.
Exemplo de um método de tratamento da exceção ^ da classe acima:
Controller depois da alteração:
Ao rodar a aplicação no Postman:
Nas variantes de inserir e atualizar dados, nós precisamos INFORMAR dados para salvar no banco.
Mas essa inserção de dados, pode causar uma exceção de dados inválidos.
Então temos 2 exceções: uma de inserir e outra de atualizar.
Nós sabemos que temos 3 validações de dados:
Nome: deve ter entre 3 e 80 caracteres;
Preço: deve ser positivo;
Descrição: não pode ter menos que 10 caracteres.
Essa ApiDocs possui todas as anotações possiveis de validação para ser utilizada!
Exemplos: @Email, @NotNull, @Positive, etc...
Precisamos inserir as dependências do maven! Hibernate e Jakarta.
Assim, com as anotações, ele verificará se os dados do JSON estão corretos.
Veja como ficarão os nossos atributos da classe ProductDTO com as anotações:
Agora, para que isso seja considerado na hora de receber a requisição, no nosso controlador, mais precisamente no nosso Post e Update, colocaremos um @Valid no parâmetro.
Isso executará uma preparação, para que sempre que o nosso Controller receber uma requisição de um Dto, ele passaa pelas verificações que fizemos com as anotações acima.
Com essas implementações, ao realiar uma pesquisa no Postman, teremos os retornos de código corretamente, mas sem a mensagem.
Para que tenhamos uma mensagem customizada, veja abaixo 👇
Para que possamos customizar as mensagens, criaremos uma classe chamada FieldMassage no pacote de dto.
Só que são várias mensagens para serem exibidas. Com isso, precisamos criar uma Lista de FieldMessage.
Criaremos uma classe chamada ValidationError. Ela será uma sub-classe de CustomError. Ou seja, terá TUDO que o CustomError tem + a lista de erros, veja:
O que acontece agora é o seguinte. Na nossa classe ControllerExceptionHandler, nós iremos alterar o método. (LEMBRE-SE, ESTE MÉTODO ALTERADO É PARA O RETORNO DO JSON)
Antes, nós instanciávamos o erro em um construtor CustomError, veja:
Nós iremos retirar o CustomError e instanciar um ValidationError no seu lugar.
Além disso, para que possamos adicionar os erros na lista, iremos fazer o seguinte.
O método MethodArgumentNotValidException, possui dentro dele uma lista de erros.
Nós iremos percorrer essa lista e adicionar dentro da nossa FieldMessage, veja:
Ao fazer a requisição no Postman:
Lembrar o que é API Rest e seus conceitos.
Instaurar padrão de camada (controller, service, repository)
Implementar DTO para busca de dados.
O Dto terá os dados que queremos utilizar, e criaremos um construtor com argumentos (com os dados inseridos).
Será utilizado para gerenciar regras de negócio, como, por exemplo, encontrar um Produto por ID, inserir, atualizar, deletar, etc.
Confira os métodos clicando acima.
Se quiser fazer busca por páginas, utilizar pageable
❗Se o método for Optional, podemos tratar diretamente no método.