Realizar requisições HTTP é uma das tarefas mais comuns em SPAs atualmente. É por meio delas que a aplicação se comunica com APIs RESTful que lhes provêem dados e funcionalidades. Portanto, saber realizar esse tipo de ação no Angular é fundamental para todo desenvolvedor que utiliza esse framework.

Atualmente, a maioria das aplicações do tipo SPA (Single Page Applications) se comunicam com algum serviço no back-end por meio do protocolo HTTP. A fim de consumir dados e funcionalidades expostos por esse serviço, que normalmente é uma API RESTful, a SPA envia requisições HTTP utilizando um dos seus diferentes verbos (GET, POST, PUT, DELETE etc.) e trata os seus possíveis resultados (200, 404, 500, etc.).

Para que possamos fazer esse tipo de requisição no JavaScript dispomos hoje de duas APIs suportadas pelos browsers modernos: XMLHttpRequest (mais antiga) e fetch (mais recente e que utiliza promises). E quando se trata de um projeto Angular, contamos com um mecanismo nativo desse framework que oferece uma interface simplificada para realizar essa tarefa, mas que internamente utiliza o XMLHttpRequest. Trata-se da classe HttpClient, disponível no módulo HttpClientModule.

Neste artigo veremos como utilizar essa classe para realizar diferentes tipos de request, bem como aprenderemos a tratar seus retornos e erros.

Conhecendo a API RESTful

Para fins de demonstração das requisições HTTP, utilizaremos uma API RESTful desenvolvida em Node.js com o framework Express. Essa API vai expor alguns endpoints para a manutenção de um cadastro de produtos (dados fixos), de acordo com a seguinte especificação da Tabela 1.

Endpoint Verbo Objetivo Retornos possíveis
/produtos GET Retorna todos os produtos. 200 sempre
/produtos/id GET Retorna um produto pelo id. 200 para sucesso 404 para id inexistente
/produtos POST Cadastra novo produto. 201 para sucesso 400 para erro de validação
/produtos/id PUT Atualiza um produto. 204 para sucesso 400 para erro de validação 404 para id inexistente
/produtos/id DELETE Exclui um produto pelo id. 204 para sucesso 404 para id inexistente

Observação: os dados são trafegados no formato JSON.

Primeiros passos

Para usar a classe HttpClient, primeiramente precisamos importar o módulo HttpClientModule no módulo em que se encontra declarado o componente/serviço em que vamos realizar as requisições. Aqui vamos realizar as requisições no próprio AppComponent, que é criado por padrão quando iniciamos um novo projeto Angular. Portanto, precisamos importar o módulo HttpClientModule no app.module.ts, da mesma forma que apresentada na Listagem 1.


  import { BrowserModule } from '@angular/platform-browser';
  import { NgModule } from '@angular/core';
  import { AppComponent } from './app.component';
  import { HttpClientModule } from '@angular/common/http';
   
  @NgModule({
    declarations: [
      AppComponent
    ],
    imports: [
      BrowserModule,
      HttpClientModule
    ],
    providers: [],
    bootstrap: [AppComponent]
  })
  export class AppModule { }
  
Listagem 1. app.module.ts
  • Linha 4: Importamos o módulo HttpClientModule do pacote @angular/common/http, de forma que ele possa ser usado nesse arquivo mais adiante;
  • Linha 12: importamos o HttpClientModule, assim a classe HttpClient poderá ser usada no componente AppComponent, que é declarado mais acima neste mesmo arquivo.

Feito isso, podemos abrir o app.component.ts e declarar no construtor da classe um parâmetro do tipo HttpClient. Note no código abaixo que usaremos o modificador private para que esse parâmetro seja automaticamente uma propriedade do componente, de forma que possamos usá-lo em seguida usando a sintaxe this.http.


  constructor(private http : HttpClient) {}
  

Como usamos o modificador private já no parâmetro, não precisamos atribuí-lo a nenhum objeto internamente. O Angular será capaz de injetar uma instância da classe HttpClient dinamicamente. Assim sendo, não precisamos usar o operador new para instanciar o objeto local http.

Configurando a interface do componente

Sabendo que temos cinco tipos de requisições possíveis na API, vamos adicionar a mesma quantidade de botões no AppComponent, um para cada request. Para isso, apagaremos todo o conteúdo do app.component.html e adicionaremos o código da Listagem 2.


  <button (click)="listarTodosProdutos()">GET</button>
  <button (click)="listarProdutoPorId()">GET /id</button>
  <button (click)="adicionarProduto()">POST</button>
  <button (click)="alterarProduto()">PUT</button>
  <button (click)="excluirProduto()">DELETE</button>
  
Listagem 2. app.component.html

Cada botão tem seu evento onclick vinculado a um método que criaremos a partir de agora.

Enviando requisições HTTP

Começaremos essa seção declarando uma variável no componente contendo o endereço da API. Ele será usado para que não precisemos repetir o endereço completo em cada requisição, bem como para facilitar sua alteração caso seja necessário:


  readonly apiURL : string;
  

A variável foi declarada como readonly, pois ela não deve ser alterada em nenhum outro ponto do código após ser inicializada, o que será feito no construtor da classe:


  constructor() {
     this.apiURL == 'http://localhost:3000';
  }
  

Em seguida, a primeira requisição que enviaremos será do tipo GET para listar todos os produtos, então vamos codificar o método listarTodosProdutos conforme a Listagem 3.


  listarTodosProdutos() {
    this.http.get(`${ this.apiURL }/produtos`)
             .subscribe(resultado => console.log(resultado));
  }
  
Listagem 3. listarTodosProdutos
  • Linha 2: com nome sugestivo, o método get da classe HttpClient é o responsável por enviar requisições do tipo GET para o endereço informado como parâmetro. Aqui esse endereço é formado pela concatenação da URL base + /produtos, conforme vimos na documentação da API.
  • Linha 3: o método get é assíncrono e retorna um Observable. Sendo assim, para recuperarmos seu resultado precisamos invocar o método subscribe, passando para ele uma função anônima cujo argumento é o corpo da resposta obtido, já devidamente convertido para objeto JavaScript.

    Na Figura 1 podemos ver o resultado que foi obtido da API sendo impresso no console do browser após clicarmos no botão “GET”:

    Resultado da requisição GET
    Figura 1.Resultado da requisição GET

    Observe que automaticamente o retorno da API é convertido de JSON para sua representação em objetos JavaScript. Nesse caso, um array contendo três itens.

    De forma semelhante ao que acabamos de fazer, podemos enviar outra requisição GET passando o id do objeto desejado como parâmetro. Faremos isso no método listarProdutoPorId (Listagem 4).

    
      listarProdutoPorId() {
        this.http.get(`${ this.apiURL }/produtos/1`)
                  .subscribe(resultado => console.log(resultado));
      }
      
    Listagem 4. Método listarProdutoPorId

    A estrutura aqui é a mesma, apenas com a adição do id no final da URL. Nesse caso, como passamos um id válido, teremos como resultado o objeto correspondente, como mostra a Figura 2.

    Resultado da
requisição GET/id
    Figura 2.Resultado da requisição GET/id

    Se ao invés de um id válido passarmos um número que não existe na lista retornada na primeira requisição, teremos como resultado um status 404 (Not Found) e um erro, como mostra a Figura 3.

    Resultado da requisição GET/id com status 404
    Figura 3.Resultado da requisição GET/id com status 404

    A primeira mensagem é um retorno padrão do browser quando ocorre uma requisição com status de erro. Já a segunda é gerada pelo Angular por termos tentado acessar o resultado de uma requisição cujo status indica um erro.

    Por ser um padrão do browser, não temos como suprimir a primeira mensagem, mas podemos tratar o erro para apresentar um retorno amigável para o usuário. Para isso, podemos informar uma segunda função como parâmetro para o método subscribe, dessa vez tendo como argumento o erro que é retornado, como mostra a Listagem 5.

    
      listarProdutoPorId() {
        this.http.get(`${ this.apiURL }/produtos/99`)
                  .subscribe(
                    resultado => {
                      console.log(resultado)
                    },
                    erro => {
                      if(erro.status == 404) {
                        console.log('Produto não localizado.');
                      }
                    }
                  );
      }
      
    Listagem 5. Tratamento de erro

    Agora somos capazes de verificar se o erro retornado é do tipo 404 (recurso não localizado). Se for, imprimimos uma mensagem amigável e a mensagem de erro original não será mais exibida, como mostra a Figura 4.

    Mensagem de erro tratado
    Figura 4.Mensagem de erro tratado

    Nesse ponto já temos o conhecimento necessário para enviar requisições e tratar tanto seu resultado, quanto seus erros. No entanto, até o momento não enviamos dados no corpo da requisição. Isso será feito na requisição POST para adicionar um novo produto, a qual é realizada no método adicionarProduto (Listagem 6).

    
      adicionarProduto() {
        var produto = { nome : "" };
       
        this.http.post(`${ this.apiURL }/produtos`, produto)
                  .subscribe(
                    resultado => {
                      console.log(resultado)
                    },
                    erro => {
                      if(erro.status == 400) {
                        console.log(erro);
                      }
                    }
                  );
      }
      
    Listagem 6. Método adicionarProduto

    Como já sabemos que em caso de sucesso o retorno da API será impresso no console, aqui declaramos a variável produto intencionalmente com o nome vazio. Isso fará com que a API retorne um status 400 (Bad Request) e então poderemos imprimir esse retorno no console, como vemos na Figura 5.

    b
    Figura 5.Retorno com status Bad Request

    Ao imprimir o retorno completo, podemos ver que ele possui vários campos que podem nos interessar. Nesse caso estamos especialmente interessados no campo error, que contém o corpo da resposta. Veja que ele contém um subcampo chamado mensagem contendo uma descrição do que ocasionou tal erro.

    Sabendo disso, podemos alterar nosso método para imprimir apenas a parte que nos interessa desse resultado:

    
      if(erro.status == 400) {
        console.log(erro.error.mensagem);
      }
      

    Se preenchermos corretamente a propriedade nome da variável produto e enviarmos a requisição, teremos como resultado o novo produto criado. E se em seguida enviarmos novamente a requisição GET para listar todos, veremos que a lista foi atualizada, como mostra a Figura 6.

    Resultados das requisições POST e GET bem sucedidos
    Figura 6.Resultados das requisições POST e GET bem sucedidos

    O método alterarProduto, responsável por enviar a requisição PUT é bem semelhante ao anterior, contando apenas com uma validação adicional para o status 404 e exibindo uma mensagem fixa em caso de sucesso, já que o status 204 (No Content) indica que o corpo da resposta vem vazio, como no código da Listagem 7.

    
      alterarProduto() {
        var produto = { id : 1, nome : "Smart TV 50 Polegadas" };
       
        this.http.put(`${ this.apiURL }/produtos/1`, produto)
                  .subscribe(
                    resultado => {
                      console.log('Produto alterado com sucesso.')
                    },
                    erro => {
                      switch(erro.status) {
                        case 400:
                          console.log(erro.error.mensagem);
                          break;
                        case 404: 
                          console.log('Produto não localizado.');
                          break;
                      }
                    }
                  );
      }
      
    Listagem 7. Método alterarProduto

    A Figura 7 mostra o resultado da requisição PUT, seguido da listagem de todos os produtos, na qual podemos ver que o item 1 foi modificado.

    Resultado das requisições PUT e GET
    Figura 7.Resultado das requisições PUT e GET

    Se informássemos um id inválido ou não preenchêssemos o nome do produto, teríamos como retorno os status 404 e 400, respectivamente.

    Por fim, temos a requisição DELETE, na qual informamos o id do produto a ser excluído, mas não passamos nada no corpo. Nesse caso, o único erro previsto ocorre se o produto não for localizado, situação em que é retornado um status 404. Na Listagem 8 temos o código do método excluirProduto.

    
      excluirProduto() { 
        this.http.delete(`${ this.apiURL }/produtos/1`)
                  .subscribe(
                    resultado => {
                      console.log('Produto excluído com sucesso.');
                    },
                    erro => {
                      if(erro.status == 404) {
                        console.log('Produto não localizado.');
                      }
                    }
                  );
      }
      
    Listagem 8. Método excluirProduto

    O resultado, como esperado, é a exclusão do produto 1 e um status 204 (No Content), por isso imprimimos uma mensagem fixa. Na Figura 8 podemos ver essa mensagem e a lista já atualizada.

    Resultado das requisições DELETE e GET
    Figura 8.Resultado das requisições DELETE e GET

    Com isso finalizamos esse estudo inicial sobre a realização de requisições HTTP no Angular usando a classe HttpClient. Esse conhecimento é basicamente o que precisamos para consumir APIs RESTful. Há, porém, uma melhoria que podemos fazer sobre esse código: como no Angular usamos TypeScript e um dos principais recursos dessa linguagem é a tipagem de dados, podemos usar isso para deixar nosso código mais claro e de fácil manutenção. Esse é o tema da próxima seção.

    Requisições tipadas

    Projetos Angular usam a linguagem TypeScript por padrão, o que nos permite desfrutar dos recursos que essa linguagem oferece. Entre eles está a tipagem estática, que é a possibilidade de fixar o tipo de uma variável no momento da sua declaração.

    Uma das possíveis aplicações para esse recurso é na definição das classes que representam os dados/entidades da nossa aplicação. Por exemplo, seguindo a ideia que desenvolvemos anteriormente, podemos criar uma classe Produto representando esse tipo que trafega entre a SPA e a API (e que possivelmente seria vinculado a inputs, por exemplo, para exibição e captura de dados em um projeto real).

    Fazendo isso, podemos tipar também o retorno das requisições HTTP, de forma que nós, ou quem precisar manter nosso código, saberemos o que esperar como resultado de cada request.

    Para exemplificar, vamos criar uma classe chamada Produto. Para isso adicionaremos uma pasta models e dentro dela o arquivo produto.model.ts ao projeto contendo o seguinte código da Listagem 9.

    
      export class Produto {
          id : number;
          nome : string;
      }
      
    Listagem 9. Classe Produto

    Em seguida, importaremos esse tipo no AppComponent para que possamos usá-lo nas requisições:

    
      import { Produto } from './models/produto.model';
      

    Feito isso, podemos agora especificar o tipo de retorno das requisições GET. No caso do método listarTodosProdutos, o tipo de retorno será Produto[], ou seja, um array de produtos. Já no listarProdutoPorId o tipo é apenas Produto, posto que apenas um item é retornado. Na Listagem 10 vemos como fica a requisição GET.

    
      listarTodosProdutos() {
        this.http.get<Produto[]>(`${ this.apiURL }/produtos`)
                  .subscribe(resultado => console.log(resultado));
      }
      
    Listagem 10. Requisição GET

    Ao fazer isso, também contamos com um auxílio a mais do editor, que passa a mostrar o tipo do resultado, como na Figura 9.

    Tipo do resultado sendo exibido pelo editor
    Figura 9.Tipo do resultado sendo exibido pelo editor

    Já na requisição POST podemos especificar o tipo tanto no corpo da requisição, quanto na resposta, assim como mostra a Listagem 11.

    
      adicionarProduto() {
        var produto = new Produto();
        produto.nome = "Cadeira Gamer";
       
        this.http.post<Produto>(`${ this.apiURL }/produtos`, produto)
        //restante do código sem alterações
      }
      
    Listagem 11. Requisição POST

    O mesmo ocorre com a requisição PUT, porém como seu retorno esperado é 204 (No Content), ela não deve ser tipada como Produto, pois o corpo da resposta virá vazio.

    Assim finalizamos este conteúdo sabendo realizar diferentes tipos de requisição HTTP no Angular, seja com objetos dinâmicos ou tipados.