<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Hassan's blog]]></title><description><![CDATA[Hassan's blog]]></description><link>https://hassanshakur.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 02 Sep 2026 16:16:14 GMT</lastBuildDate><atom:link href="https://hassanshakur.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Generate and Download PDFs From HTML Templates using Node.js (Express) & Puppeteer.]]></title><description><![CDATA[In a freelancing project I was working on last week, I encountered this feature, and it took me quite a while to collect information from different sources to be able to implement it. So I just decided to document the procedures. Hope it enlightens s...]]></description><link>https://hassanshakur.hashnode.dev/how-to-generate-and-download-pdfs-from-html-templates-using-nodejs-express-puppeteer</link><guid isPermaLink="true">https://hassanshakur.hashnode.dev/how-to-generate-and-download-pdfs-from-html-templates-using-nodejs-express-puppeteer</guid><category><![CDATA[pdf]]></category><category><![CDATA[Express]]></category><category><![CDATA[puppeteer]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Hassan Shakur]]></dc:creator><pubDate>Tue, 06 Feb 2024 19:33:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707247689690/2f2f4191-ec74-41da-89b2-d63ae9671e5e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a freelancing project I was working on last week, I encountered this feature, and it took me quite a while to collect information from different sources to be able to implement it. So I just decided to document the procedures. Hope it enlightens someone :).</p>
<p>PDFs are awesome because, well, they are portable whilst maintaining their format. I'm going to be using Express as it's one of the most popular and powerful frameworks for building web apps in the JS world. NextJS on the front end is just my choice, but you can go with whatever you enjoy working with. All you need is to send a request anyway, right? Enough chitchat...</p>
<h2 id="heading-server-setup-express">Server Setup (Express)</h2>
<p>For this, you will need to install a couple of things. To begin create your project folder and inside it, another folder named <code>server</code>. Then in your terminal navigate to your server folder location and run:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># first initialize npm</span>
npm init -y

<span class="hljs-comment"># then install server &amp; dev dependencies</span>
npm install nodemon express cors
</code></pre>
<p>Then create a simple server and handle <code>cors</code> accurately:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/app.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cors'</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = <span class="hljs-number">7000</span>;

app.use(
  cors({
    <span class="hljs-attr">origin</span>: <span class="hljs-string">'http://localhost:3000'</span>, <span class="hljs-comment">// we will set up our frontend at port 3000</span>
  })
);

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">'Hello, world!'</span>);
});

app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>Now ensure that is working and set it aside for a while:</p>
<pre><code class="lang-bash">nodemon ./app.js <span class="hljs-comment"># Server is running on http://localhost:7000</span>
</code></pre>
<h2 id="heading-frontend-setup-nextjs">Frontend Setup (NextJS)</h2>
<p>Nextjs is a full-stack framework built on the React library and that's what we'll be using. Open your project folder in the terminal and run the following to generate a new nextJs app (currently at version 14.0.1):</p>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>Ensure you have <code>node.js</code> installed for the above commands to work. This will ask you a couple of questions including the project's name. Just go with the defaults, or choose whatever you prefer.</p>
<hr />
<h2 id="heading-connecting-backend-to-frontend">Connecting Backend to Frontend</h2>
<p>Let's test what we have so far. In your next.js app, navigate to <code>app/page.tsx</code> (or <code>.jsx</code> if you went with the <code>JavaScript</code> option). Remove everything in this file and create a very simple component that sends a request to our backend and displays whatever response it receives. This would be a great point to install <code>axios</code> to help us with the requests:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># In your frontend app terminal</span>
npm i axios
</code></pre>
<p>Awesome. Then add this to your <code>app/page.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// client/app/page.tsx</span>

<span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-comment">// where our server is running</span>
<span class="hljs-keyword">const</span> BACKEND_URL = <span class="hljs-string">'http://localhost:7000'</span>;

<span class="hljs-keyword">const</span> Home = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// to store the message from the server</span>
  <span class="hljs-keyword">const</span> [message, setMessage] = useState(<span class="hljs-string">''</span>);

  <span class="hljs-comment">// fetch data from the server</span>
  <span class="hljs-keyword">const</span> fetchData = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.get(BACKEND_URL);
      setMessage(res.data);
    } <span class="hljs-keyword">catch</span> (err) {
      setMessage(<span class="hljs-string">'Error occurred'</span>);
      <span class="hljs-built_in">console</span>.error(err);
    }
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchData();
  }, []);

  <span class="hljs-comment">// render the message from the server</span>
  <span class="hljs-keyword">return</span> &lt;div&gt;{message || <span class="hljs-string">'Fetching...'</span>}&lt;/div&gt;;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home;
</code></pre>
<p>The code is quite simple. It sends a single request to our backend at the port <code>7000</code> and renders the message received.</p>
<p>Now you have to run both the frontend and backend servers in 2 different terminals. In your frontend terminal, run:</p>
<pre><code class="lang-bash">npm run dev <span class="hljs-comment"># dev server by default listens at port 3000</span>
</code></pre>
<p>Then in your backend terminal:</p>
<pre><code class="lang-bash">nodemon ./app.js <span class="hljs-comment"># in our case opens port 7000</span>
</code></pre>
<p>Now open your favorite browser and visit <code>localhost:3000</code>. If all goes well, you should see <code>Fetching...</code> for a moment, then the text <code>Hello, world!</code> that came from the backend. If you see any errors to do with something <code>CORS</code> check to ensure that your frontend URL is the same one passed in the <code>origin</code> option when setting up the cors in the server.</p>
<hr />
<h2 id="heading-pdf-backend-init">PDF Backend Init</h2>
<p>Now let's set up a few things in the backend to ensure it is ready for pdf generation. In this example, we will use <code>puppeteer</code> which is:</p>
<blockquote>
<p><strong>a Node library that provides a high-level API to control headless Chrome over the DevTools Protocol.</strong></p>
</blockquote>
<p>To use this, you are going to have to download <code>Chromium</code> for whatever OS you are using. Download it on this page, then move it to a preferred location on your PC and extract it:</p>
<p><a target="_blank" href="https://download-chromium.appspot.com/">Download Chromium from Appspot</a></p>
<p>Now copy the path to the Chrome executable <code>chrome.exe</code> as we are about to use it. Now install the following in your backend:</p>
<pre><code class="lang-bash">npm i puppeteer
</code></pre>
<p>Now we can create a <code>html</code> template to be used in generating the pdf. This is just simple HTML with some <code>internal CSS</code> styling. Inside your <code>server</code> folder, create a <code>templates</code> folder and add a <code>sample.html</code> file with some content as:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- server/templates/sample.html --&gt;</span>

<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
      <span class="hljs-selector-tag">body</span> {
        <span class="hljs-attribute">max-width</span>: <span class="hljs-number">800px</span>;
        <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> auto;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
        <span class="hljs-attribute">font-family</span>: Arial, sans-serif;
      }

      <span class="hljs-selector-tag">header</span> {
        <span class="hljs-attribute">text-align</span>: center;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
        <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f2f2f2</span>;
      }

      <span class="hljs-selector-tag">h1</span> {
        <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>;
      }

      <span class="hljs-selector-tag">table</span> {
        <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
        <span class="hljs-attribute">border-collapse</span>: collapse;
        <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">20px</span>;
      }

      <span class="hljs-selector-tag">th</span>,
      <span class="hljs-selector-tag">td</span> {
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
        <span class="hljs-attribute">text-align</span>: left;
        <span class="hljs-attribute">border-bottom</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#ddd</span>;
      }

      <span class="hljs-selector-tag">footer</span> {
        <span class="hljs-attribute">text-align</span>: center;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
        <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f2f2f2</span>;
      }
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">header</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Sample Header<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Content<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This is a sample content.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">thead</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>ID<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Name<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Age<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Major<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span>&gt;</span>
        {{DataHere}}
      <span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">footer</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Sample Footer<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">footer</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>This is a simple page with a <code>header</code>, content with a <code>table</code> and a <code>footer</code> at the bottom. The <code>{{DataHere}}</code> is a placeholder where we will include our data.</p>
<p>Almost there... Now create a new file - <code>helper.js</code> that will hold a <code>generatePDF</code> helper function that will handle the PDF generation logic. Then add the following code to it:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/helper.js</span>

<span class="hljs-keyword">const</span> generatePDF = <span class="hljs-keyword">async</span> (htmlTemplate, pdfName, pdfFilePath) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Create a browser instance</span>
    <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch({
      <span class="hljs-attr">headless</span>: <span class="hljs-string">'new'</span>,
      <span class="hljs-attr">executablePath</span>:
        <span class="hljs-string">'c:\\Users\\Hassan\\Development\\drivers\\chrome-win\\chrome.exe'</span>, <span class="hljs-comment">// set your chrome.exe path here</span>
    });

    <span class="hljs-comment">// Create a new page</span>
    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();

    <span class="hljs-comment">// Load your HTML template into the page</span>
    <span class="hljs-keyword">await</span> page.setContent(htmlTemplate, {
      <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">'domcontentloaded'</span>,
    });

    <span class="hljs-comment">// Emulate screen media type</span>
    <span class="hljs-keyword">await</span> page.emulateMediaType(<span class="hljs-string">'screen'</span>);

    <span class="hljs-comment">// Wait for fonts to load</span>
    <span class="hljs-keyword">await</span> page.evaluateHandle(<span class="hljs-string">'document.fonts.ready'</span>);

    <span class="hljs-comment">// Get a PDF buffer from the page</span>
    <span class="hljs-keyword">const</span> pdfBytes = <span class="hljs-keyword">await</span> page.pdf({
      <span class="hljs-attr">path</span>: pdfFilePath,
      <span class="hljs-attr">printBackground</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">format</span>: <span class="hljs-string">'A4'</span>,
      <span class="hljs-attr">displayHeaderFooter</span>: <span class="hljs-literal">true</span>,
    });

    <span class="hljs-comment">// Write the PDF buffer to a file</span>
    <span class="hljs-keyword">await</span> fs.promises.writeFile(pdfFilePath, pdfBytes);

    <span class="hljs-comment">// Close the browser</span>
    <span class="hljs-keyword">await</span> browser.close();

    <span class="hljs-comment">// Return the name of the file that was saved</span>
    <span class="hljs-keyword">return</span> pdfName;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error, <span class="hljs-string">'Error generating PDF'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }
};

<span class="hljs-built_in">module</span>.exports = generatePDF;
</code></pre>
<p>This code is as straightforward as it gets. We create a browser instance, create a new page, load our HTML template into the page, and then generate a PDF from the page. We then write the PDF to a file and return the name of the file that was saved. If an error occurs, we log it to the console and return null.</p>
<p>Now all that is left in the backend is to set up a route and its handler. So let's quickly do that. Adjust your <code>app.js</code>, to look like this:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/app.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cors'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

<span class="hljs-keyword">const</span> generatePDF = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./helper'</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = <span class="hljs-number">7000</span>;

<span class="hljs-comment">// parse req body</span>
app.use(express.json());

app.use(
  cors({
    <span class="hljs-attr">origin</span>: <span class="hljs-string">'http://localhost:3000'</span>,
  })
);

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">'Hello, world!'</span>);
});

app.post(<span class="hljs-string">'/generate-pdf'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-comment">// get the students array from req body</span>
  <span class="hljs-keyword">const</span> students = req.body;

  <span class="hljs-comment">// read the html template</span>
  <span class="hljs-keyword">const</span> htmlTemplate = fs.readFileSync(
    path.resolve(__dirname, <span class="hljs-string">'./templates/sample.html'</span>),
    <span class="hljs-string">'utf-8'</span>
  );

  <span class="hljs-comment">// create a folder to store the pdfs</span>
  <span class="hljs-keyword">const</span> pdfFolderPath = path.resolve(__dirname, <span class="hljs-string">'./pdfs'</span>);
  <span class="hljs-comment">// create the folder if it doesn't exist</span>
  <span class="hljs-keyword">if</span> (!fs.existsSync(pdfFolderPath)) {
    fs.mkdirSync(pdfFolderPath);
  }

  <span class="hljs-comment">// create a unique pdf name</span>
  <span class="hljs-keyword">const</span> pdfName = <span class="hljs-string">`students_<span class="hljs-subst">${<span class="hljs-built_in">Date</span>.now()}</span>.pdf`</span>;
  <span class="hljs-keyword">const</span> pdfFilePath = path.join(pdfFolderPath, pdfName);

  <span class="hljs-comment">// create rows with students data</span>
  <span class="hljs-keyword">const</span> tableRows = students
    .map(
      <span class="hljs-function">(<span class="hljs-params">student, id</span>) =&gt;</span> <span class="hljs-string">`
    &lt;tr&gt;
      &lt;td&gt;<span class="hljs-subst">${id}</span>&lt;/td&gt;
      &lt;td&gt;<span class="hljs-subst">${student.name}</span>&lt;/td&gt;
      &lt;td&gt;<span class="hljs-subst">${student.age}</span>&lt;/td&gt;
      &lt;td&gt;<span class="hljs-subst">${student.major}</span>&lt;/td&gt;
    &lt;/tr&gt;
  `</span>
    )
    .join(<span class="hljs-string">''</span>);

  <span class="hljs-comment">// replace the placeholder in html template with actual table rows with student data</span>
  <span class="hljs-keyword">const</span> html = htmlTemplate.replace(<span class="hljs-string">'{{DataHere}}'</span>, tableRows);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// generate pdf</span>
    <span class="hljs-keyword">const</span> file = <span class="hljs-keyword">await</span> generatePDF(html, pdfName, pdfFilePath);

    <span class="hljs-keyword">if</span> (!file) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Error generating PDF'</span>);
    }

    <span class="hljs-comment">// convert pdf to base64</span>
    <span class="hljs-keyword">const</span> bitmap = <span class="hljs-keyword">await</span> fs.promises.readFile(pdfFilePath);
    <span class="hljs-keyword">const</span> pdfBase64 = Buffer.from(bitmap).toString(<span class="hljs-string">'base64'</span>);

    <span class="hljs-comment">// unlink the file - optional - deletes file from server</span>
    <span class="hljs-keyword">if</span> (fs.existsSync(pdfFilePath)) {
      fs.unlinkSync(pdfFilePath);
    }

    <span class="hljs-comment">// send the base64 pdf as response</span>
    res.send({
      <span class="hljs-attr">file</span>: pdfBase64,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Success!'</span>,
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
    res.status(<span class="hljs-number">500</span>).send(<span class="hljs-string">'Error generating PDF'</span>);
  }
});

app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>A few things are happening here:</p>
<ul>
<li><p>We are first importing the <code>fs</code> &amp; <code>path</code> modules inbuilt in <code>node</code> which are used in managing files and file paths respectively. We are also importing the <code>generatePDF</code> function we created earlier.</p>
</li>
<li><p>The <code>app.use(express.json());</code> at line 12 is an express <code>middleware</code> allowing us to extract data from the request body. You can read more about middleware <a target="_blank" href="https://expressjs.com/en/guide/using-middleware.html">here</a>.</p>
</li>
<li><p>From line 24 - 88 is our controller for anything <code>POSTed</code> in the <code>/generate-pdf</code> path in our backend. The controller fetches data from the request body and reads and replaces the placeholder text with actual mapped data in our sample template. It then generates the PDF using the template HTML through our <code>generatePDF</code> function, converts it to <code>base64</code> suitable for transfer (and smaller size), deletes the created server pdf, and sends the converted version.</p>
</li>
</ul>
<hr />
<p>Now that we have the controller ready, let's trigger it from our front end. Back to the <code>page.tsx</code>, replace what we previously had in our component, and add the following:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// client/app/page.tsx</span>

<span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-comment">// where our server is running</span>
<span class="hljs-keyword">const</span> BACKEND_URL = <span class="hljs-string">'http://localhost:7000'</span>;

<span class="hljs-comment">// sample list of students each with a name, age and major</span>
<span class="hljs-keyword">const</span> students = [
  {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Yuqee Chen'</span>,
    <span class="hljs-attr">age</span>: <span class="hljs-number">21</span>,
    <span class="hljs-attr">major</span>: <span class="hljs-string">'Computer Science'</span>,
  },
  {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Jane Doe'</span>,
    <span class="hljs-attr">age</span>: <span class="hljs-number">20</span>,
    <span class="hljs-attr">major</span>: <span class="hljs-string">'Engineering'</span>,
  },
  {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Tiffany Wei'</span>,
    <span class="hljs-attr">age</span>: <span class="hljs-number">22</span>,
    <span class="hljs-attr">major</span>: <span class="hljs-string">'Business'</span>,
  },
];

<span class="hljs-keyword">const</span> Home = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// to store the message from the server</span>
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-comment">// fetch data from the server</span>
  <span class="hljs-keyword">const</span> fetchData = <span class="hljs-keyword">async</span> () =&gt; {
    setIsLoading(<span class="hljs-function">() =&gt;</span> <span class="hljs-literal">true</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">`<span class="hljs-subst">${BACKEND_URL}</span>/generate-pdf`</span>,
        students
      );

      <span class="hljs-keyword">const</span> a = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'a'</span>);

      <span class="hljs-comment">// Set the href attribute to the data URL of the base64 file</span>
      a.href = <span class="hljs-string">'data:application/pdf;base64,'</span> + data.file;

      <span class="hljs-comment">// Set the download attribute to the file name</span>
      a.download = <span class="hljs-string">'my-server-doc.pdf'</span>;

      <span class="hljs-comment">// Trigger the download by clicking the element</span>
      a.click();
    } <span class="hljs-keyword">catch</span> (err) {
      setError(<span class="hljs-string">'Error occurred'</span>);
      <span class="hljs-built_in">console</span>.error(err);
    }
    setIsLoading(<span class="hljs-function">() =&gt;</span> <span class="hljs-literal">false</span>);
  };

  <span class="hljs-comment">// render the message from the server</span>
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">'button'</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{fetchData}</span>&gt;</span>
        {isLoading ? 'Downloading...' : 'Click to download pdf!'}
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

      {error &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{error}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home;
</code></pre>
<p>What happens here is also quite straightforward:</p>
<ul>
<li><p>Add 2 <code>useStates</code> to handle <code>error</code> and <code>loading</code> states.</p>
</li>
<li><p>We then add a list of students' data.</p>
</li>
<li><p>Create a <code>fetchData</code> function that sends a <code>POST</code> request to our backend with the static list of students above.</p>
</li>
<li><p>It then creates an <code>anchor</code> element pointing to download the <code>base64</code> pdf data restructured from the backend response, and this link is clicked dynamically through the code to download the pdf - <code>a.click()</code>.</p>
</li>
<li><p>We then have a <code>button</code> that when clicked calls the <code>fetchData</code> and shows its loading and success states.</p>
</li>
</ul>
<p>Finally, the setup is done. All that remains is to make sure the 2 servers are up and running, then visit <code>localhost:3000</code> and click the button. The magic will happen and pdf downloaded with our data.</p>
<hr />
<p>The source code is in my GitHub repo at <a target="_blank" href="https://github.com/hassanShakur/pdf-downloader">hassanShakur/pdf-downloader</a>.</p>
<hr />
<p>And that's it. Hope you suffered enough reading this first article of mine. I enjoy seeing people s... Forget it. Hope you enjoyed the show. Till later InshaAllah.</p>
]]></content:encoded></item></channel></rss>