Download de PDF via POST com HttpClient no Angular

Veja como utilizar o HttpClient para obter o conteúdo de arquivo PDF de uma API, mostrá-lo na tela e criar um link para download do arquivo.

Banner de publicação do post: Download de PDF - Como mostrar e efetuar o download de um PDF utilizando HttpClient do Angular
Como mostrar e efetuar o download de um PDF utilizando HttpClient do Angular

Incluir um link (<a>) em seu HTML para iniciar um download é uma prática comum de implementar. No entanto, em outro cenário no qual já precisei trabalhar, o frontend precisava enviar algumas informações no body e no header de uma requisição POST. Essas informações eram processadas pelo backend, que então retornava um arquivo PDF. Este cenário não é tão direto quanto o primeiro exemplo. Portanto, gostaria de compartilhar como podemos exibir e disponibilizar o download de um arquivo PDF através de uma solicitação POST usando o HttpClient do Angular.

Estruturando o cenário

O cenário é bem simples, vou criar um simples backend que será executado em Node.js e depois um novo projeto Angular.

Criando o backend para fornecer o PDF

Para criar o projeto utilizei o script abaixo:

# Crio um diretório:
mkdir backend
cd backend

# Inicializa o pacote NPM
npm init -y

Dentro deste diretório (backend):

  1. criei o arquivo index.js
  2. modifiquei o package.json
  3. copiei um arquivo PDF chamado dummy.pdf
import { createServer } from "node:http";
import { createReadStream } from "fs";

const hostname = "127.0.0.1";
const port = 3000;

const server = createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "http://localhost:4200");
  res.setHeader("Access-Control-Request-Method", "POST");
  res.setHeader("Access-Control-Allow-Headers", "*");

  if (req.method === "OPTIONS") {
    res.statusCode = 204;
    res.end();
    return;
  }

  const arquivoPDF = createReadStream("./dummy.pdf");

  res.statusCode = 200;
  res.setHeader("Content-Type", "application/pdf");
  arquivoPDF.pipe(res);
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

index.js

💡
O código da linha 8 à 16 trata de evitar problemas de CORS (Cross-Origin Resource Sharing) no navegador, já que estamos executando o frontend em localhost:4200 e o backend em localhost:3000. Se você não estiver familiarizado com CORS, recomendo a leitura deste link para entender melhor o conceito.
{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "license": "ISC"
}

package.json

Para testar o código, utilizei um navegador para realizar uma requisição GET em localhost:3000 e, em seguida, utilizei o comando curl para testar o método POST no mesmo endereço.

Navegador mostrando o PDF retornado pelo endereço localhost:3000
Testando o fornecimento do arquivo PDF através de um GET
curl -X POST localhost:3000 --output download.pdf

Testando o fornecimento do arquivo PDF através de um POST

Agora que está tudo certo no backend, vamos criar nosso projeto Angular.

Criando o projeto frontend com Angular

Angular CLI: 17.2.0
Node: 18.18.2
Package Manager: npm 10.3.0
OS: darwin arm64

Versões utilizadas

Para criar o projeto:

ng new --inline-template false --skip-git --skip-tests --minimal --strict frontend-pdf
💡
Se quiser saber mais sobre os parâmetros do comando acima, digite no seu terminal o seguinte: ng new help

Para testar o projeto:

cd frontend-pdf 
npm run start
Navegador mostrando o conteúdo inicial do projeto Angular no endereço localhost:4200
Projeto recém-criado em execução

Utilizando o HttpClient para efetuar a requisição

Agora que o projeto está criado, vou importar o módulo do HttpClient e realizar um POST para ver o que acontece:

import { HttpClient, HttpClientModule } from "@angular/common/http";
import { Component } from "@angular/core";
import { RouterOutlet } from "@angular/router";

@Component({
  selector: "app-root",
  standalone: true,
  imports: [HttpClientModule, RouterOutlet],
  templateUrl: "./app.component.html",
})
export class AppComponent {
  constructor(private httpClient: HttpClient) {}

  download() {
    const body = {};
    this.httpClient
      .post("http://localhost:3000", body)
      .subscribe((response) => {
        console.log(response);
      });
  }
}

app.component.ts

<button (click)="download()">Download</button>

app.component.html

O resultado no navegador será:

Navegador mostrando o conteúdo de localhost:4200. O console do DevTools está mostrando um erro: Failure during parsing for http://localhost:3000
Erro: Failure during parsing for http://localhost:3000

Por padrão, o HttpClient espera que a resposta da requisição seja um JSON válido. Se a resposta não for um JSON válido, o HttpClient lançará o erro "Failure during parsing for http://localhost:3000". Como nossa API não retorna um JSON, mas sim bytes de um arquivo PDF, é necessário especificar o tipo de resposta esperada por meio do parâmetro responseType:

  download() {
    const body = {};
    this.httpClient
      .post("http://localhost:3000", body, {
        responseType: "arraybuffer",
      })
      .subscribe((response) => {
        console.log(response);
      });
  }

trecho do arquivo app.component.ts

Executando a requisição novamente, temos o seguinte resultado:

Navegador mostrando no DevTools a seguinte mensagem: ArrayBuffer { byteLength: 13764 }
Mensagem no console mostrando os bytes obtidos na requisição

Mostrando o PDF em um iframe

Agora que temos o PDF acessível, isto é, o conteúdo do PDF está armazenado na variável response, podemos criar uma URL exclusiva dentro do navegador utilizando o método window.URL.createObjectURL(). Podemos então vincular essa URL ao conteúdo do PDF e atribuí-la a um elemento <iframe>.

💡
O createObjectURL é um método JavaScript que cria um URL temporário para objetos de dados, como arquivos Blob ou File. Esse URL temporário permite que você acesse os dados desses objetos como se fossem recursos de URL, o que pode ser útil para exibir imagens, vídeos e outros tipos de mídia diretamente no navegador, sem a necessidade de upload para um servidor. É comumente utilizado em aplicações da web para pré-visualização de arquivos ou para trabalhar com conteúdo dinâmico.

Veja a implementação a seguir:

import {
  HttpClient,
  HttpClientModule,
} from "@angular/common/http";
import { Component } from "@angular/core";
import {
  DomSanitizer,
  SafeUrl,
} from "@angular/platform-browser";
import { RouterOutlet } from "@angular/router";

@Component({
  selector: "app-root",
  standalone: true,
  imports: [HttpClientModule, RouterOutlet],
  templateUrl: "./app.component.html",
})
export class AppComponent {
  url: SafeUrl | null = null;

  constructor(
    private httpClient: HttpClient,
    private sanitizer: DomSanitizer
  ) {}

  download() {
    const body = {};
    this.httpClient
      .post("http://localhost:3000", body, {
        responseType: "arraybuffer",
      })
      .subscribe((response) => {
        // Cria um Blob representando o PDF
        // a partir do response
        const pdfBlob = new Blob([response], {
          type: "application/pdf",
        });

        // Cria uma URL temporária
        // para o Blob usando createObjectURL
        const temporaryUrl =
          window.URL.createObjectURL(pdfBlob);

        // Torna a URL segura para uso
        // no iframe utilizando DomSanitizer
        this.url =
          this.sanitizer.bypassSecurityTrustResourceUrl(
            temporaryUrl
          );
      });
  }
}

app.component.ts

<button (click)="download()">Download</button>

@if (url) {
<div>
  <iframe
    [src]="url"
    style="width: 800px; height: 600px"
  ></iframe>
</div>
}

app.component.html

Resultado obtido em dois navegadores, Firefox Developer Edition e Safari:

Navegador Firefox mostrando o PDF renderizado em um iframe
Resultado obtido no Firefox Developer Edition
Navegador Safari mostrando o PDF renderizado em um iframe
Resultado obtido no Safari

Efetuando o download do PDF

Uma outra alternativa para disponibilizar o PDF ao usuário é o download. Então ao invés de exibir o conteúdo do PDF em um elemento específico, criei dinamicamente um elemento (<a>) e atribuí o valor da URL temporária ao atributo "href". Para garantir o download automático quando o elemento <a> for clicado, adicionei o atributo "download", fornecendo como valor o nome sugerido para o arquivo. Finalmente, realizei um clique programático para iniciar o download.

Confira a implementação abaixo:

download() {
  const body = {};
  this.httpClient
    .post("http://localhost:3000", body, {
      responseType: "arraybuffer",
    })
    .subscribe((response) => {
      // Cria um Blob representando o PDF
      // a partir do response
      const pdfBlob = new Blob([response], {
        type: "application/pdf",
      });

      // Cria uma URL temporária
      // para o Blob usando createObjectURL
      const temporaryUrl =
        window.URL.createObjectURL(pdfBlob);

      // Torna a URL segura para uso
      // no iframe utilizando DomSanitizer
      // this.url =
      //   this.sanitizer.bypassSecurityTrustResourceUrl(
      //     temporaryUrl
      //   );

      const temporaryAnchor = document.createElement("a");
      temporaryAnchor.href = temporaryUrl;

      // Indicamos no `<a>` o atributo download
      // com o nome do arquivo:
      temporaryAnchor.download = `arquivo-${Date.now()}.pdf`;

      // ou se quiser abrir o conteúdo em uma nova
      // aba, comente a linha acima e descomente
      // a linha abaixo:
      // temporaryAnchor.target = "_blank";

      document.body.appendChild(temporaryAnchor);
      temporaryAnchor.click();
      temporaryAnchor.remove();
    });
}

trecho do app.component.ts

Navegador mostrando que quando o usuário clica no botão Download, o PDF é baixado
Efetuando o download de um PDF com HttpClient do Angular

Considerações

Este texto demonstra como podemos adquirir e compartilhar um arquivo PDF através de uma API. No entanto, esses princípios são facilmente aplicáveis a outros formatos de arquivo, como XLS, CSV, JPG, e muitos outros.

Além de explorarmos o uso do responseType do HttpClient, também empregamos o window.URL.createObjectURL para a criação de URLs de objetos.

É relevante mencionar que, no contexto de arquivos PDF, existem outras opções para exibição direta no frontend, como a biblioteca PDF.js, que pode ser uma alternativa viável.

Links interessantes: