Welcome folks today in this blog post we will be generating the pdf invoice
document generator using the html5 template in javascript. All the full source code of the application is shown below.
Get Started
In order to get started you need to initialize a new node.js
project using the below command as shown below
npm init -y
npm i express
npm i dotenv
npm i convert-html-to-pdf
As you can see that we are installing the above dependencies to convert the html5 template to pdf invoice document.
Now we need to make the index.js
file which will be the starting point of the application as shown below
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
const express = require("express") const app = express() app.use(express.json()) const PORT = 11000 const deliveryOptions = { logo: "https://thumbs.dreamstime.com/b/laundry-basket-icon-trendy-design-style-isolated-white-background-vector-simple-modern-flat-symbol-web-site-mobile-135748439.jpg", name: "Company Name", address1: "Some Road No 1", address2: "Some State, Pincode", orderId: "INV-001", customerName: "Sai Sandeep", date: "Oct 2, 2022", paymentTerms: "Delivery Items Receipt", items: [ { name: "SINGLE BED_SHEET", qty: 3, rate: "10.00", amount: "30.00" }, { name: "DOUBLE BED_SHEET", qty: 2, rate: "20.00", amount: "40.00" }, { name: "TOWELS", qty: 3, rate: "5.00", amount: "15.00" }, { name: "CLOTHES", qty: 3, rate: "50.00", amount: "150.00" } ], total: "235.00", balanceDue: "235.00", notes: "Thanks for being an awesome customer!", terms: "This invoice is auto generated at the time of delivery. If there is any issue, Contact provider" } app.get("/", (req,res)=> { res.status(200).send({msg: "Hi there, welcome to Invoice API. Go to /sample route to get sample data"}) }) app.get("/sample", (req,res) => { res.status(200).send(deliveryOptions) }) app.listen(PORT, () => { console.log(`Listening on PORT ${PORT}`) }) |
As you can see we are starting out the express app at the port number 11000. And then we are starting this app at that port number.
And also we have the /
route. Inside that we are returning the sample json data where we will be returning the basic message to the user with the instructions on how to use this api.
And also we have the second /sample
route where we are sending the json
response where we are sending the delivery options. Inside the delivery options we have the sample json data in which we have the different information which will be used to create the invoice pdf document. It contains the details about the user and the products.
Making the POST Request to Generate the PDF Invoice
And now we will be defining the post request to generate the pdf invoice document and download it inside the local disk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
app.post("/getInvoice", (req,res) => { const result = verifyBody(req.body) if(result.success){ getInvoice(req.body).then(pdf => { res.status(200) res.contentType("application/pdf"); res.send(pdf); }).catch(err => { console.error(err) res.status(500).send({success: false, error: "something went wrong"}) }) } else { res.status(400).send(result) } }) |
And now as you can see we have the post request inside that we are first of all getting the request payload data which is sent from the client side to this post request. And first of all we are writing a custom function which is used to validate the request data object which is coming from the client to the server.
Creating the Validating Function to Verify the Request Data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
function verifyBody(data){ if(!data?.logo){ return { success: false, msg: "logo is missing" } } if(!data?.name){ return { success: false, msg: "name is missing" } } if(!data?.address1){ return { success: false, msg: "address1 is missing" } } if(!data?.address2){ return { success: false, msg: "address2 is missing" } } if(!data?.orderId){ return { success: false, msg: "orderId is missing" } } if(!data?.customerName){ return { success: false, msg: "customerName is missing" } } if(!data?.paymentTerms){ return { success: false, msg: "paymentTerms is missing" } } if(!data?.date){ return { success: false, msg: "date is missing" } } if(!data?.items){ return { success: false, msg: "items is missing" } } for(let i = 0; i < data.items.length; i++){ if(!data.items[i]?.name){ return { success: false, msg: `items.${i}.name is missing` } } if(!data.items[i]?.qty){ return { success: false, msg: `items.${i}.qty is missing` } } if(typeof data.items[i].qty !== "number"){ return { success: false, msg: `items.${i}.qty is needs to be number` } } if(!data.items[i]?.rate){ return { success: false, msg: `items.${i}.rate is missing` } } if(isNaN(parseFloat(data.items[i].rate))){ return { success: false, msg: `items.${i}.rate is not a valid number` } } if(!data.items[i]?.amount){ return { success: false, msg: `items.${i}.amount is missing` } } if(isNaN(parseFloat(data.items[i].amount))){ return { success: false, msg: `items.${i}.amount is not a valid number` } } } if(!data?.total){ return { success: false, msg: `total is missing` } } if(isNaN(parseFloat(data?.total))){ return { success: false, msg: `total is not a valid number` } } if(!data?.balanceDue){ return { success: false, msg: `balanceDue is missing` } } if(isNaN(parseFloat(data?.balanceDue))){ return { success: false, msg: `balanceDue is not a valid number` } } if(!data?.notes){ return { success: false, msg: "notes is missing" } } if(!data?.terms){ return { success: false, msg: "terms is missing" } } return { success: true } } |
And inside the above function we are checking for all the properties which needs to be present. Here we are using the if condition to validate the request data object. And if all the properties are present then we are returning the boolean property success to true.
And now we will be writing the getInvoice() method which is taking the request data object to convert the html to invoice pdf document. So you need to create a separate file called invoice.js
file and copy paste the below code
invoice.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
const HTMLToPDF = require('convert-html-to-pdf').default; function getDeliveryItemsHTML(items){ let data = "" for(let item of items){ data += ` <div class="table-row"> <div class=" table-cell w-6/12 text-left font-bold py-1 px-4">${item.name}</div> <div class=" table-cell w-[10%] text-center">${item.qty}</div> <div class=" table-cell w-2/12 text-center">₹${item.rate}</div> <div class=" table-cell w-2/12 text-center">₹${item.amount}</div> </div> ` } return data } function getDeliveryHTML(options){ return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Invoice</title> <style> /*! tailwindcss v3.0.12 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.flex{display:flex}.table{display:table}.table-cell{display:table-cell}.table-header-group{display:table-header-group}.table-row-group{display:table-row-group}.table-row{display:table-row}.hidden{display:none}.w-60{width:15rem}.w-40{width:10rem}.w-full{width:100%}.w-\[12rem\]{width:12rem}.w-9\/12{width:75%}.w-3\/12{width:25%}.w-6\/12{width:50%}.w-2\/12{width:16.666667%}.w-\[10\%\]{width:10%}.flex-1{flex:1 1 0%}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.justify-center{justify-content:center}.rounded-l-lg{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.border-x-\[1px\]{border-left-width:1px;border-right-width:1px}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.p-10{padding:2.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-4{padding-left:1rem}.pb-20{padding-bottom:5rem}.pb-16{padding-bottom:4rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pt-20{padding-top:5rem}.pr-10{padding-right:2.5rem}.pl-24{padding-left:6rem}.pb-6{padding-bottom:1.5rem}.pl-10{padding-left:2.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-normal{font-weight:400}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))} </style> </head> <body> <div class="p-10"> <!--Logo and Other info--> <div class="flex items-start justify-center"> <div class="flex-1"> <div class="w-60 pb-6"> <img class="w-40" src="${options.logo}" alt="Logo"> </div> <div class="w-60 pl-4 pb-6"> <h3 class="font-bold">${options.name}</h3> <p>${options.address1}</p> <p>${options.address2}</p> </div> <div class="pl-4 pb-20"> <p class="text-gray-500">Bill To:</p> <h3 class="font-bold">${options.customerName}</h3> </div> </div> <div class="flex items-end flex-col"> <div class="pb-16"> <h1 class=" font-normal text-4xl pb-1">Delivery Report</h1> <p class="text-right text-gray-500 text-xl"># ${options.orderId}</p> </div> <div class="flex"> <div class="flex flex-col items-end"> <p class="text-gray-500 py-1">Date:</p> <p class="text-gray-500 py-1">Payment Terms:</p> <p class="font-bold text-xl py-1 pb-2 ">Balance Due:</p> </div> <div class="flex flex-col items-end w-[12rem] text-right"> <p class="py-1">${options.date}</p> <p class="py-1 pl-10">${options.paymentTerms}</p> <div class="pb-2 py-1"> <p class="font-bold text-xl">₹${options.balanceDue}</p> </div> </div> </div> </div> </div> <!--Items List--> <div class="table w-full"> <div class=" table-header-group bg-gray-700 text-white "> <div class=" table-row "> <div class=" table-cell w-6/12 text-left py-2 px-4 rounded-l-lg border-x-[1px]">Item</div> <div class=" table-cell w-[10%] text-center border-x-[1px]">Quantity</div> <div class=" table-cell w-2/12 text-center border-x-[1px]">Rate</div> <div class=" table-cell w-2/12 text-center rounded-r-lg border-x-[1px]">Amount</div> </div> </div> <div class="table-row-group"> ${getDeliveryItemsHTML(options.items)} </div> </div> <!--Total Amount--> <div class=" pt-20 pr-10 text-right"> <p class="text-gray-400">Total: <span class="pl-24 text-black">₹${options.total}</span></p> </div> <!--Notes and Other info--> <div class="py-6"> <p class="text-gray-400 pb-2">Notes:</p> <p>${options.notes}</p> </div> <div class=""> <p class="text-gray-400 pb-2">Terms:</p> <p>${options.terms}</p> </div> </div> </body> </html> ` } async function getInvoice(options) { return new Promise(async (resolve,reject) => { try { const html = getDeliveryHTML(options) const htmlToPDF = new HTMLToPDF(html) const pdf = await htmlToPDF.convert({waitForNetworkIdle: true, browserOptions: {defaultViewport: {width: 1920, height: 1080}}, pdfOptions: {height: 1200, width:900, timeout: 0}}) resolve(pdf) } catch(err){ reject(err) } }) } module.exports = { getInvoice } |
As you can see we are importing the convert-html-to-pdf
library and then we are using this library to convert the html template to pdf document. And then we are passing the html template to pdf document using the htmltopdf() constructor. And then we are using the convert()
method to export html to pdf. And then we are exporting this method at the bottom.
And then we need to import this file at the top of the file in index.js
file as shown below
1 |
const {getInvoice} = require("./invoice"); |
Full Source Code
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
const express = require("express") const {getInvoice} = require("./invoice"); const app = express() app.use(express.json()) const PORT = 11000 const deliveryOptions = { logo: "https://thumbs.dreamstime.com/b/laundry-basket-icon-trendy-design-style-isolated-white-background-vector-simple-modern-flat-symbol-web-site-mobile-135748439.jpg", name: "Company Name", address1: "Some Road No 1", address2: "Some State, Pincode", orderId: "INV-001", customerName: "Sai Sandeep", date: "Oct 2, 2022", paymentTerms: "Delivery Items Receipt", items: [ { name: "SINGLE BED_SHEET", qty: 3, rate: "10.00", amount: "30.00" }, { name: "DOUBLE BED_SHEET", qty: 2, rate: "20.00", amount: "40.00" }, { name: "TOWELS", qty: 3, rate: "5.00", amount: "15.00" }, { name: "CLOTHES", qty: 3, rate: "50.00", amount: "150.00" } ], total: "235.00", balanceDue: "235.00", notes: "Thanks for being an awesome customer!", terms: "This invoice is auto generated at the time of delivery. If there is any issue, Contact provider" } function verifyBody(data){ if(!data?.logo){ return { success: false, msg: "logo is missing" } } if(!data?.name){ return { success: false, msg: "name is missing" } } if(!data?.address1){ return { success: false, msg: "address1 is missing" } } if(!data?.address2){ return { success: false, msg: "address2 is missing" } } if(!data?.orderId){ return { success: false, msg: "orderId is missing" } } if(!data?.customerName){ return { success: false, msg: "customerName is missing" } } if(!data?.paymentTerms){ return { success: false, msg: "paymentTerms is missing" } } if(!data?.date){ return { success: false, msg: "date is missing" } } if(!data?.items){ return { success: false, msg: "items is missing" } } for(let i = 0; i < data.items.length; i++){ if(!data.items[i]?.name){ return { success: false, msg: `items.${i}.name is missing` } } if(!data.items[i]?.qty){ return { success: false, msg: `items.${i}.qty is missing` } } if(typeof data.items[i].qty !== "number"){ return { success: false, msg: `items.${i}.qty is needs to be number` } } if(!data.items[i]?.rate){ return { success: false, msg: `items.${i}.rate is missing` } } if(isNaN(parseFloat(data.items[i].rate))){ return { success: false, msg: `items.${i}.rate is not a valid number` } } if(!data.items[i]?.amount){ return { success: false, msg: `items.${i}.amount is missing` } } if(isNaN(parseFloat(data.items[i].amount))){ return { success: false, msg: `items.${i}.amount is not a valid number` } } } if(!data?.total){ return { success: false, msg: `total is missing` } } if(isNaN(parseFloat(data?.total))){ return { success: false, msg: `total is not a valid number` } } if(!data?.balanceDue){ return { success: false, msg: `balanceDue is missing` } } if(isNaN(parseFloat(data?.balanceDue))){ return { success: false, msg: `balanceDue is not a valid number` } } if(!data?.notes){ return { success: false, msg: "notes is missing" } } if(!data?.terms){ return { success: false, msg: "terms is missing" } } return { success: true } } app.post("/getInvoice", (req,res) => { const result = verifyBody(req.body) if(result.success){ getInvoice(req.body).then(pdf => { res.status(200) res.contentType("application/pdf"); res.send(pdf); }).catch(err => { console.error(err) res.status(500).send({success: false, error: "something went wrong"}) }) } else { res.status(400).send(result) } }) app.get("/sample", (req,res) => { res.status(200).send(deliveryOptions) }) app.get("/", (req,res)=> { res.status(200).send({msg: "Hi there, welcome to Invoice API. Go to /sample route to get sample data"}) }) app.listen(PORT, () => { console.log(`Listening on PORT ${PORT}`) }) |