Introduction
Invoicing is the backbone of business operations, but as companies grow, client-side methods are just not tenable — they introduce security risks and performance limitations.
Can server-side invoice generation help, then? Is it faster? How well does it integrate with my existing tools? Is it worth it?
In this article, we’ll explore those questions and guide you through implementing them with Node.js and the Apryse SDK. We’ll need no other third-party dependencies or plugins.
Let’s dive in!
Benefits of Server-Side Invoice Generation
Why generate invoices on the server?
Right away, you score wins in both security and scalability. First, keeping business logic and processing on the server means sensitive information never leaves your secure, controlled environment. Secondly, because servers are built to handle complex tasks, they can generate thousands of invoices in seconds based on computing power, far outpacing client-side solutions. For example, Zerodha — one of India’s top stock broker companies, generates 1.5+ million PDFs in 25 minutes.
Then there are the bonuses: real-time updates, reducing human error, or the ability to set up your system to automatically generate invoices based on specific events or schedules, for example.
However, while server-side PDF generation offers several advantages, using the wrong tools can lead to significant challenges. This is where Apryse shines, providing a seamless and effortless solution for generating PDFs with ease.
Let’s discuss what Apryse is and why you should care.
What is Apryse and Why Use it?
Apryse, formerly known as PDFTron, is a leading provider of document processing technology for developers and enterprises. The company offers a comprehensive suite of tools and technologies designed to handle various aspects of document processing, catering to a wide range of needs in the digital document landscape.
Specifically for our needs, it provides a powerful Node.js SDK designed for server-side document processing, including PDF generation. The SDK supports creating PDFs from templates, which is precisely what we need. We’ll design invoice templates and populate them with data dynamically.
Let’s move forward and discuss how to implement server-side invoice generation.
Implementing Server-Side Invoice Generation with Node.js and Apryse SDK
We’ll first need to set up the development environment to get started. We’ll need Node.js installed on the local machine and an Apryse SDK API key for this tutorial.
But if you are curious and want to check the code directly, check out this Github link.
Setting Up the Environment and Obtaining the API Key from Apryse
To start, open a new folder in your favorite text editor and initialize it with NPM by running the command npm init -y. This will create a new package.json file in the root directory. Now, let’s install the necessary packages. For this article, we’ll need these packages:
dotenv
: This package helps you load environment variables from a .env file into process.env.@pdftron/pdfnet-node
: This package is the official package provided by Apryse and will be used to generate the PDFs.
To install the packages from the terminal, run the following command:
npm i dotenv @pdftron/pdfnet-node
To obtain the Apryse API key, go to dev.apryse.com and sign in.
For non-production use, this trial key is free. It doesn’t expire, allowing you to test Apryse’s features thoroughly (with a watermark added to generated PDFs) until you’re satisfied with the product.
This will install the required packages. Now, create a file called .env in the root folder. We’ll be storing the Apryse API key here.
APRYSE_API_KEY=demo:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Let’s move to the next step now.
Creating an Invoice Template
Instead of struggling with template creation in libraries like jsPDF, you can simply design your invoice in Microsoft Word or Google Docs. The DOCX file then serves as your template for dynamic PDF generation.
We’ll use this template as the base, but we need to make some changes. To use the same template, open the File tab and click the Make a copy option. Once copied, we can update the template.
First, let’s take a look at the template:
Apryse uses something called template keys. Template keys are special placeholders in your document that Apryse uses to insert dynamic content. These keys are denoted by double curly braces, like this: {{ key }}
.
Here’s how it works:
- In your document template, you place these keys wherever you want dynamic content to appear.
- When generating the document, you provide a JSON dictionary to the API.
- Each key in your JSON should match a template key in your document.
- Apryse then replaces each template key with the corresponding value from your JSON.
For example, if your template has {{company_name}}
, your JSON should include:
{
"company_name": "Acme Inc."
}
This system is flexible and can handle various types of data. However, it’s important to ensure your template and JSON match correctly to avoid errors. If there’s a mismatch or invalid data, Apryse will throw an exception with a helpful error message to guide you.
In the above template, you can see keys like company_name
, company_address
, invoice_date
, etc. However, conditionals are also used. For example, this line {{if postal_code}} {{postal_code}}{{endif}}
means that the postal code will only be included in the generated document if it exists in the provided JSON data.
Here’s a breakdown of how this conditional works:
{{if postal_code}}
checks if the postal_code key exists in the JSON data and has a non-empty value.- If postal_code exists and has a value, the content between
{{if postal_code}}
and{{endif}}
will be included in the final document. - In this case, that content is
{{postal_code}}
, which will be replaced with the actual postal code value. - If
postal_code
doesn’t exist or is empty, everything between{{if postal_code}}
and{{endif}}
will be omitted from the final document.
This conditional approach allows for flexible template design, accommodating scenarios where certain information might not always be available. By omitting empty fields rather than leaving blank spaces, it helps create cleaner, more professional-looking documents.
Feel free to apply similar conditionals for other optional fields in your invoice template! This way, your generated documents will always have a polished appearance and include only the information that truly matters.
We can also use loops in our template to handle dynamic content, such as itemized lists on an invoice. The section shown below demonstrates how to use a loop to display all the items in the document:
This loop structure allows for the flexible display of multiple items without knowing in advance how many there will be. The {{loop items}}
tag indicates the start of the loop, and {{endloop}}
marks its end. Everything between these tags will be repeated for each item in the ‘items’
array provided in your JSON data.
Within the loop, you can access the properties of each item using template keys like {{item}}
, {{description}}
, {{quantity}}
, and so on. These keys correspond to the properties of each object in your ‘items’
array. As the loop iterates, it replaces these keys with the actual values for each item, creating a row for every product or service in your invoice.
This approach is beneficial for invoices, as it allows you to create a single template that can accommodate any number of line items. Whether your invoice has one item or a hundred, the template will adapt automatically, generating a professional and accurate document every time.
Once you are ready with the template, you can download it in DOCX format from File → Download → Microsoft Word (.docx). Copy this template to your project directory’s root folder.
Creating our data
Now, create a new file called data.json inside the root directory and paste the following data into it:
{
"company_name": "Tech Solutions Ltd.",
"company_address": "123 Tech Street",
"company_city": "Palo Alto",
"company_country": "USA",
"postal_code": "94301",
"invoice_number": "INV-2024-001",
"date": "2024-12-01",
"due_date": "2024-12-15",
"items": [
{
"item": "Web Development",
"description": "Web Development Services",
"quantity": "10",
"price": "100.0",
"tax_percent": "10.0",
"amount": "1100.0"
},
{
"item": "Cloud Hosting",
"description": "Cloud Hosting (1 Year)",
"quantity": "1",
"price": "200.0",
"tax_percent": "5.0",
"amount": "210.0"
},
{
"item": "Support",
"description": "Support & Maintenance",
"quantity": "5",
"price": "50.0",
"tax_percent": "8.0",
"amount": "270.0"
}
],
"total_due": "1580.0",
"signature": "Authorized Signatory"
}
If you examine this JSON file closely, you’ll see that the keys match those used in the document template.
Now, we’re ready to move on to PDF creation.
Creating the Invoice Generation Script
Create a new file called main.js
inside the root folder of your project. Copy the downloaded DOCX template and move it to this folder. Now, paste the following code into the main.js
file:
const { PDFNet } = require("@pdftron/pdfnet-node");
const fs = require("fs");
const dotenv = require("dotenv");
// Load environment variables
dotenv.config();
// Ensure API key is present
if (!process.env.APRYSE_API_KEY) {
console.error(
"Error: APRYSE_API_KEY is not defined in the environment variables."
);
process.exit(1);
}
/**
* Loads JSON data from a file.
* @param {string} filePath - Path to the JSON file.
* @returns {object} Parsed JSON object.
*/
function loadJsonData(filePath) {
try {
const data = fs.readFileSync(filePath, "utf8");
return JSON.parse(data);
} catch (error) {
console.error(`Error loading JSON data from ${filePath}:`, error.message);
process.exit(1); // Exit with error
}
}
/**
* Generates a PDF from a template and JSON data.
* @param {string} inputFile - Path to the input Office file.
* @param {string} outputFile - Path to save the generated PDF.
* @param {string} jsonFile - Path to the JSON data file.
*/
async function generatePDF(inputFile, outputFile, jsonFile) {
try {
console.log("Initializing PDF generation...");
// Initialize OfficeToPDFOptions
const options = new PDFNet.Convert.OfficeToPDFOptions();
// Create a TemplateDocument object from the input Office file
const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath(
inputFile,
options
);
// Load JSON data
const jsonData = loadJsonData(jsonFile);
const jsonDataString = JSON.stringify(jsonData);
// Fill the template with JSON data
const pdfDoc = await templateDoc.fillTemplateJson(jsonDataString);
// Save the filled template as a PDF
await pdfDoc.save(outputFile, PDFNet.SDFDoc.SaveOptions.e_linearized);
console.log(`PDF successfully saved to ${outputFile}`);
} catch (error) {
console.error("An error occurred during PDF generation:", error.message);
}
}
// Run the script
(async () => {
const inputFile = process.argv[2] || "./template.docx"; // Default input file
const outputFile = process.argv[3] || "./output.pdf"; // Default output file
const jsonFile = process.argv[4] || "data.json"; // Default JSON file
try {
await PDFNet.runWithCleanup(
() => generatePDF(inputFile, outputFile, jsonFile),
process.env.APRYSE_API_KEY
);
} catch (error) {
console.error("PDFNet initialization error:", error.message);
} finally {
await PDFNet.shutdown();
}
})();
Let’s understand this code now:
- We designate a template using
PDFNet.Convert.createOfficeTemplateWithPath
and pass it to our DOCX template. Apryse can use templates in .docx, .xlsx, .pptx, .doc, .ppt, and .xls formats. - Within the
generatePDF
function, our JSON file is accessed through theloadJsonData
function. - The template is filled with data using
templateDoc.fillTemplateJson
, which binds the JSON data to the placeholders in the template. - The completed PDF document is saved to the specified output file path using
pdfDoc.save
with a linearized format for efficient loading on the web. - The main script is wrapped in an immediately invoked function or IIFE. It extracts file paths for the input template, output PDF, and JSON data from command-line arguments. If arguments are not provided, default values are used (
template.docx
,output.pdf
, anddata.json
). - The script initializes the PDFNet library with the
APRYSE_API_KEY
usingPDFNet.runWithCleanup
, ensuring it is ready for operations. In fact, thegeneratePDF
function is called within the cleanup block, which callsPDFNet.shutdown()
in the end, to release resources.
Now, with the data.json
file and the template.docx
file(assuming your template file is named template.docx
) in place, you can run the script using the command:
node main.js
If you want, you can also dynamically pass values like:
node main.js ./input.docx ./output.pdf ./data.json
Where the input.docx
is your input file, output.pdf
is your output file name, and data.json
is your JSON data file.
If the program runs successfully, you should be able to see these messages in the console:
❯ node main.js
PDFNet is running in demo mode.
Initializing PDF generation...
Package: office
Package: base
PDF successfully saved to ./output.pdf
And a new file will be created in your root directory called output.docx
. The PDF should look like this:
The image above shows that all the template values are dynamically replaced with those provided in the JSON file.
What’s Next?
You might face an issue where the fonts do not display as expected. In that case, you’ll need to embed the fonts into your document. See this to know more. For this, you’ll need Microsoft Office installed on your machine. From there, you can change the settings from File -> Options -> Save -> Check Embed fonts in the file.
To learn more about embedding fonts, refer to this article.
You can also embed images into your PDF files dynamically. Let’s say you want to attach a signature in place of the Authorized Signatory text. In your data.json
file, replace the signature part with this:
"signature": {
"image_url": "https://www.clipartkey.com/mpngs/m/196-1968543_signature-image-john-doe.png",
"height": 60,
"width": 100
}
Here, the image_url
represents the link of the image, and the height
and the width
represent the height and width of the image. The image will now replace the signature.
Here’s a screenshot:
Conclusion
Server-side invoice generation is ideal for businesses looking to streamline their workflow without compromising security, performance, or seamless integration with existing systems. By leveraging template-based generation with any office tool of your choice and dynamic data insertion, you can create professional, customized invoices efficiently and at scale.
Now you know how to dynamically create PDFs using JSON data, Node.js, and the Apryse SDK. By choosing Apryse for your document processing needs, you’re opting for a robust, secure, and flexible solution that can handle complex document workflows while providing a seamless experience for developers and end-users.
Get started with Apryse today.