Download de PDF via POST com HttpClient no 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):
- criei o arquivo index.js
- modifiquei o package.json
- 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
{
"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.
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
Para testar o projeto:
cd frontend-pdf
npm run start
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á:
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:
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>
.
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:
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
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: