How to Programmatically Fill a PDF Form with Raw JavaScript (The Easy Way)

Dec 22, 2025

Dec 22, 2025

programmatically fill a pdf with raw JS
programmatically fill a pdf with raw JS
programmatically fill a pdf with raw JS

Programmatically filling PDF forms can be straightforward or tricky, depending on the setup. The challenge usually depends on whether you are writing everything from scratch, using a JavaScript library, how the form fields are named, and how well those names match your data model.

A more subtle challenge comes from the limitations of the libraries you choose. Most PDF form automation tools work by directly manipulating AcroForm fields, which means you are often responsible for validation, may need layout workarounds for repeating data, and have limited ability to modify or restructure fields once they are embedded in the PDF.

When populating PDF form fields with structured datasets like the following JSON data:

// service-request.json
[
  {
    "firstName": "John",
    "lastName": "Doe",
    "emailAddress": "john.doe@example.com",
    "phoneNumber": "+1-555-123-4567",
    "type": "Installation",
    "priority": "High",
    "description": "Need assistance setting up and configuring a new server rack in the data center.\\nThis includes mounting all necessary equipment, installing power distribution units, managing cable organization, and ensuring proper ventilation and power balance.\\n\\nAfter the physical setup is complete, please verify all network connections, confirm switch and port configurations, and ensure that each device in the rack can successfully communicate across the network.\\nProvide detailed documentation of configurations, cabling, and connectivity test results once verification is complete."
  }
]

Most libraries simply map data into AcroForm fields and then flatten the PDF into static elements. Unlike most tools, Joyfill Form Builder and Filler SDK provides a more flexible abstraction layer that supports defining and validating fields, updating their structure over time, and conditionally showing or hiding elements, all without being tied to the PDF internal layout.

This guide shows how to use Joyfill to programmatically fill PDF forms in Node.js. We will cover reading fields, inserting data, and saving the PDF to create a reproducible workflow.

Why use Joyfill to Programmatically fill PDF Forms?

Traditional JavaScript PDF libraries focus on direct field manipulation. You parse the PDF, find matching field identifiers, inject values, then flatten everything. This approach works for small and stable forms, but it quickly breaks down as complexity grows.

A major limitation comes from the underlying PDF technologies themselves. PDF forms are built on two incompatible architectures: AcroForms and XFA (XML Forms Architecture). Most libraries only support one of them, usually AcroForms. As a result, XFA fields are often ignored entirely, leaving sections of your form blank when filled programmatically.

Even when fields are supported, developers still run into challenges when:

  • Field names do not map cleanly to the data model

  • The form structure needs to evolve

  • Layout changes frequently

  • Conditional visibility or computed values are required

  • Business logic must live outside the PDF binary

Joyfill removes these constraints by representing PDF forms as a JoyDoc. The PDF becomes a visual layer, while the JoyDoc serves as the source of truth for field structure, data, metadata, and layout. Instead of interacting with brittle PDF internals, developers work with a predictable JSON model that remains stable, even when templates change. This is especially valuable when the same dataset needs to populate multiple document formats.

Key advantages for developers

You work with a JSON schema rather than PDF internals

JoyDoc represents your form in a structured JSON format. Each page, field, and resource is addressable through identifiers, making it simple to fetch, update, or compose documents through code rather than manual layout editing.

Every field has a stable identifier

Instead of guessing where values belong, you reference clear identifiers that do not change even if the document layout moves. This lowers maintenance cost and removes the need for repeated parsing.

Metadata lets you enrich documents

You can attach metadata to documents, pages, and fields. This enables structured workflows like tagging fields for export, versioning forms, or passing custom rules along with templates.

Formulas give fields logic

Formulas allow fields to compute values from other fields. For example, summing rows, handling default values, or applying conditional logic. No additional code is required for these computed results once the formulas are defined.

Better adaptability over time

If the PDF form changes, the identifiers and JSON structure allow you to update the document without rewriting your automation logic. You are not locked into the PDF internal layout.

Consistent PDF Forms

Most problems with programmatically filling PDF forms start long before any code runs. They are usually caused by how the form was authored. A poorly structured PDF form can make automation painful, no matter how clean your code is.

Standardize Form Structure Through JoyDoc

With Joyfill, this complexity disappears entirely. Forms created through Joyfill are represented as JoyDocs, a unified format that provides a consistent, predictable structure regardless of the underlying PDF technology. You don't need to worry about incompatible form types, fields that fail to populate, or data that vanishes during processing. Everything is managed through a single, stable model that keeps your form definitions and data perfectly aligned, every time.

For example, to create a PDF form using Joyfill, start by setting up a Joyfill form builder environment.

  1. Create a new folder named joyform and open it in your editor of choice.

  2. Install the project dependencies using your preferred package manager.

    npm
  3. Add a views/builder.ejs file to the project.

    This file will serve as the main page for working with your form. It can load an existing template or create a new one when you open it in the browser.

    <!-- views/builder.ejs -->
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Form Builder</title>
      <script src=" <https://cdn.jsdelivr.net/npm/@joyfill/components@latest/dist/joyfill.min.js>"></script>
    </head>
    
    <body>
      <div id="joyfill"></div>
      <script>
        <% if (form) { %>
          const doc = <%- form %>;
        <% } %>
    
        <% if (!form) { %>
          const doc = Joyfill.getDefaultDocument();
          doc.name = <%- JSON.stringify(name) %>
        <% } %>
    
              Joyfill.JoyDoc(
                document.getElementById('joyfill'),
                {
                  doc,
                  mode: 'edit',
                  onChange: async (changelogs, updatedDoc) => {
                    // Log document changes to the console.
                    console.log(changelogs);
    
                    // Persist changes
                    try {
                      await fetch('/form', {
                        method: 'POST',
                        body: JSON.stringify(updatedDoc),
                        headers: {
                          'Content-Type': 'application/json',
                        },
                      });
    
                      console.log(`Successfully updated ${updatedDoc.name}`);
                    } catch (e) {
                      console.error(e);
                      alert(`Failed to update ${updatedDoc.name}: ${e.message}`);
                    }
                  },
                }
              );
    
      </script>
    </body>
    
    </html>
  4. Add an index.js file to the project and save the following script as its contents:

    // index.js
    const { JSONFilePreset } = require('lowdb/node');
    const express = require('express');
    
    (async () => {
      // Create a data store for JoyDoc PDF form storage
      const db = await JSONFilePreset('db.json', {
        form: [],
      });
    
      // Configure an express application
      const app = express();
    
      app.set('view engine', 'ejs');
    
      app.use(express.json());
      app.use(express.urlencoded({ extended: false }));
    
      // Retrieve a PDF form for editing
      app.get('/form/:name', async (req, res) => {
        const { name } = req.params;
        let form = db.data.form.find((d) => d.name === name);
    
        res.render('builder', { form: JSON.stringify(form), name });
      });
    
      // Create or update form
      app.post('/form', async (req, res) => {
        const form = req.body;
        const formIndex = db.data.form.findIndex((d) => d.name === form.name);
    
        if (formIndex === -1) {
          // Save new PDF form
          db.data.form.push(form);
          await db.write();
    
          res.end();
          return;
        }
    
        // Update existing form
        db.data.form[formIndex] = form;
        db.write();
    
        res.status(200).end();
      });
    
      const port = 3000;
    
      app.listen(port, () => {
        console.log(`Form builder is listening on port ${port}`);
      });
    })();
  5. Run the script to start the express server.

    node
  6. Visit http://localhost:3000/form/service-request in your web browser. You should see the following PDF form builder.


    pdf form builder preview

Use Consistent, Meaningful Field Names

Automation depends on predictable field names. If your fields are named things like text1, text2, or field3, you will waste time figuring out what maps where during initial development and subsequent maintenance. To avoid this pitfall, always use clear, predictable names that match your data model (e.g., firstName, emailAddress, priority, etc.).

A few minutes spent creating a proper PDF form with consistent naming and accessible fields will save hours of debugging later. The cleaner the source form, the simpler the automation.

For example, add fields that you would normally find on a service request form to the form and give each field a human-friendly name through the identifier field as shown below.

pdf names and identifiers

At this point, the content of the project’s db.json file should look like the following:

// db.json
{
  "form": [
    {
      "_id": "69042cc6f71bde2d6304d0f1",
      "identifier": "doc_69042cc6f71bde2d6304d0f1",
      "name": "service-request",
      // form fields,
      "fields": [
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042cd8ca31952ab8f9c979",
          "type": "block",
          "title": "Heading Text",
          "value": "Service Request",
          "identifier": "field_69042cd8ca31952ab8f9c979"
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042d04784f9cfbe61c5849",
          "type": "text",
          "title": "First Name",
          "identifier": "firstName"
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042d4e901859cad7a2e670",
          "type": "text",
          "title": "Phone Number",
          "identifier": "phoneNumber"
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042d6c44cc56d50f44fdbc",
          "type": "text",
          "title": "Email Address",
          "identifier": "emailAddress"
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042d7d758727e69c32a92d",
          "type": "text",
          "title": "Last Name",
          "identifier": "lastName"
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042e0ea372aeb632c9421b",
          "type": "dropdown",
          "title": "Service Type",
          "options": [
            {
              "_id": "69042cc675376b64eddda857",
              "value": "Yes",
              "deleted": true
            },
            {
              "_id": "69042cc6a82115b235236847",
              "value": "No",
              "deleted": true
            },
            {
              "_id": "69042cc6ec92f77bcb4a3f65",
              "value": "N/A",
              "deleted": true
            },
            {
              "_id": "69042e4713ed9ae0610ce289",
              "value": "Installation",
              "width": 100,
              "deleted": false
            },
            {
              "_id": "69042e66949791af17b4b077",
              "value": "Maintenance & Repair",
              "width": 100,
              "deleted": false
            },
            {
              "_id": "69042e73c4be2641732f4cb1",
              "value": "Consultation",
              "width": 100,
              "deleted": false
            },
            {
              "_id": "69042e7eba7a43ca20422a1e",
              "value": "Technical Support",
              "width": 100,
              "deleted": false
            }
          ],
          "identifier": "type",
          "value": ""
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042eaefd0e2580a68764a8",
          "type": "multiSelect",
          "title": "Priority",
          "multi": false,
          "options": [
            {
              "_id": "69042cc6dd4dbf98302c7fdd",
              "value": "Low",
              "deleted": false
            },
            {
              "_id": "69042cc6c63dca3d9dfc82f6",
              "value": "Medium",
              "deleted": false
            },
            {
              "_id": "69042cc66bc177e4f7fdb55f",
              "value": "High",
              "deleted": false
            },
            {
              "_id": "69042f57f9713f03bacca8dd",
              "value": "Urgent",
              "width": 0,
              "deleted": false
            }
          ],
          "identifier": "priority"
        },
        {
          "file": "69042cc6913ba9fae414b0b4",
          "_id": "69042f96d4d94ea2103d9754",
          "type": "textarea",
          "title": "Service Description",
          "identifier": "description",
          "value": ""
        }
      ]
    }
  ]
}

Programmatically Fill the PDF Form

Now that we have a properly structured fillable form in the form of a JoyDoc, the next step is to map data to each form field. This data can come from JSON objects, database records, or other sources within your application. The goal remains the same: populate each PDF form field with its corresponding data field.

In the case of our current example project, using the service-requests.json file from the opening section of this post should suffice.

  1. Add a fill-forms.js file to the project and save the following as its contents:

    // fill-forms.js
    // Retrieve service request PDF form
    const form = require('./db.json').form.find(
      (f) => f.name === 'service-request'
    );
    // Retrieve service requests from data source
    const requests = require('./service-requests.json');
    const puppeteer = require('puppeteer');
    const { join } = require('path');
    const { existsSync, mkdirSync, writeFileSync } = require('fs');
    
    async function generatePDFs(requests) {
      // Create output folder for filled out PDFs
      if (!existsSync('pdfs')) {
        mkdirSync('pdfs');
      }
    
      const browser = await puppeteer.launch();
      const page = await browser.newPage();
    
      await page.addScriptTag({
        url: '<https://cdn.jsdelivr.net/npm/@joyfill/components@latest/dist/joyfill.min.js>',
      });
    
      // Fill each form in a headless browser and save it to a PDF file.
      for (let req of requests) {
        const populatedForm = fillFormFields(req, form);
    
        console.log(`Filling out ${req.firstName} ${req.lastName}'s form`);
    
        await page.evaluate((populatedForm) => {
          const container = document.createElement('div');
          container.id = 'joyfill';
          document.body.appendChild(container);
          document.body.appendChild(container);
          Joyfill.JoyDocExporter(container, {
            doc: populatedForm,
            config: { page: { height: 1056, width: 816, padding: 0 } },
          });
        }, populatedForm);
    
        await page.pdf({
          path: join('pdfs', `${populatedForm.name}.pdf`),
        });
    
        await page.evaluate(() => {
          document.querySelector('#joyfill').remove();
        });
      }
    
      await browser.close();
    }
    
    function fillFormFields(request, form) {
      const filledDocument = { ...form };
    
      // Set the PDF document's name
      filledDocument.name = `${filledDocument.name} - ${request.firstName} ${request.lastName}`;
    
      // Dynamically populate fields
      for (let field of filledDocument.fields) {
        const id = field.identifier;
    
        switch (field.type) {
          case 'multiSelect':
            const selected = field.options.find(
              (option) => option.value.trim() === request[id].trim()
            );
    
            field.value = [selected._id];
            break;
          case 'dropdown':
            const option = field.options.find(
              (option) => option.value.trim() === request[id].trim()
            );
    
            field.value = option._id;
            break;
          default:
            field.value = request[id];
        }
      }
    
      return filledDocument;
    }
    
    generatePDFs(requests)
      .then(() => {
        console.log('PDFs filled successfully');
      })
      .catch((e) => {
        console.log('Failed to fill PDFs', e);
      });
  2. Add headless browser support to the project by running the following command:

    npm
  3. Run the fill-forms.js script.

    npm

    This should fill out the form fields, flatten the fields as static elements, and save them to binary PDF files in the pdf subfolder of the project.

Filling Other Form Field Types

The sample project in this post shows how to programmatically populate text fields, dropdowns, and single-choice options. If you need to support additional field types, you would extend the logic in the fill-forms.js script. In practice, this means updating the switch statement to recognize each new field type and apply the appropriate handling behavior.

    switch (field.type) {
      case 'multiSelect':
        const selected = field.options.find(
          (option) => option.value.trim() === request[id].trim()
        );

        field.value = [selected._id];
        break;
      case 'dropdown':
        const option = field.options.find(
          (option) => option.value.trim() === request[id].trim()
        );

        field.value = option._id;
        break;
      case 'date':
        field.value = new Date(request.date).getTime();
        break;
      case 'image':
      case 'file':
        const imageBuffer = fs.readFileSync(request.photoAttachment);
        field.value = imageBuffer.toString('base64');
        break;
      case 'table':
        // Where the value of request.table has the following interface
        // Array<{
        //   _id: string; // row id
        //   deleted: boolean;
        //   cells: {
        //     [cell_id: string]: string; // column id
        //   };
        // }>
        field.value = request.table;
        break;
      case 'chart':
        // Where each point has x (x axis coordinate), y (y coordiante), and a label field.
        field.value[0].points = request.chart.map((point) => ({
          ...point,
          _id: Joyfill.generateObjectId(),
        }));
        break;
      default:
        field.value = request[id];
    }

Conclusion

Filling PDF forms with JavaScript is much simpler when you work with consistent structure instead of raw PDF internals. Joyfill makes this possible by standardizing forms as JoyDoc, keeping fields, metadata, and logic organized in one place.

This gives you reliable identifiers, predictable behavior, and flexibility as forms evolve. If you want a cleaner, more maintainable way to automate PDFs, Joyfill is an easy way to get there.

Need to build PDF capabilities inside your SaaS application? Joyfill helps developers embed native PDF and form experiences directly into their SaaS apps.

PDF

SDK

javascript

node.js

Arinze the author

Written by Arinze

form builder sign up background

Getting started is easy

Schedule a demo call with our experts to learn more about Joyfill.

Forms as powerful as an app.

© 2025 Joyfill LLC. All rights reserved.

form builder sign up background

Getting started is easy

Schedule a demo call with our experts to learn more about Joyfill.

Forms as powerful as an app.

© 2025 Joyfill LLC. All rights reserved.

form builder sign up background

Getting started is easy

Schedule a demo call with our experts to learn more about Joyfill.

Forms as powerful as an app.

© 2025 Joyfill LLC. All rights reserved.