This guide walks you through building a Node.js API that accepts an invoice PDF, extracts the data, and returns it as a CSV file. You’ll learn how to set up the project, parse the PDF, map the fields, and handle common formatting issues. By the end, you’ll have a reusable endpoint that saves hours of manual data entry.
What You’ll Need
- Node.js (version 14 or later) installed on your machine
- npm (comes with Node.js)
- A code editor (like VS Code)
- Basic familiarity with JavaScript and Express
- Sample invoice PDFs to test with (you can create simple ones or use any you have)
Step-by-Step
- Initialize your project
Create a new folder and run
npm init -yto generate a package.json file. Then install the required packages:npm install express multer pdf-parse csv-writerThese packages handle the HTTP server, file uploads, PDF text extraction, and CSV generation respectively.
- Set up the Express server
Create an
index.jsfile and set up a basic Express app with a POST route that accepts a file upload. Use Multer to handle the multipart form data:const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer({ dest: 'uploads/' }); app.post('/convert', upload.single('invoice'), (req, res) => { // conversion logic goes here }); app.listen(3000, () => console.log('Server running on port 3000')); - Extract text from the PDF
Use
pdf-parseto read the uploaded PDF. The library returns the raw text content, which you can then parse. Add this inside the route handler:const fs = require('fs'); const pdf = require('pdf-parse'); const dataBuffer = fs.readFileSync(req.file.path); pdf(dataBuffer).then(function(data) { const text = data.text; // process text }).catch(err => res.status(500).send('PDF parsing failed')); - Parse the invoice fields
Invoices vary, but most contain key fields like invoice number, date, vendor name, line items, and total. You’ll need to extract these using regex or string matching. For example:
function extractInvoiceData(text) { const invoiceNum = text.match(/Invoice\s*#?\s*([A-Z0-9-]+)/i)?.[1]; const date = text.match(/Date\s*:?\s*(\d{1,2}\/\d{1,2}\/\d{2,4})/i)?.[1]; const total = text.match(/Total\s*:?\s*\$?([0-9,.]+)/i)?.[1]; return { invoiceNum, date, total }; }For line items, you’ll need a more complex pattern—often splitting by lines and looking for patterns like quantity, description, unit price, and amount.
- Structure the CSV data
Decide on the CSV columns. A common structure is:
InvoiceNumber,Date,Vendor,ItemDescription,Quantity,UnitPrice,AmountCreate an array of objects, each representing a row. For the header row, you can include the invoice-level info repeated on every line, or have separate sections. For simplicity, we’ll use one row per line item with invoice-level data repeated.
- Generate the CSV file
Use
csv-writerto create the CSV. It takes a header array and an array of records:const createCsvWriter = require('csv-writer').createObjectCsvWriter; const csvWriter = createCsvWriter({ path: 'output.csv', header: [ {id: 'invoiceNum', title: 'InvoiceNumber'}, {id: 'date', title: 'Date'}, {id: 'vendor', title: 'Vendor'}, {id: 'desc', title: 'ItemDescription'}, {id: 'qty', title: 'Quantity'}, {id: 'price', title: 'UnitPrice'}, {id: 'amount', title: 'Amount'} ] }); await csvWriter.writeRecords(records); - Return the CSV to the client
Instead of saving to a file, you can send the CSV directly as a response. After generating the records, set the appropriate headers and send the CSV string:
const csvString = await csvWriter.writeRecords(records).then(() => fs.readFileSync('output.csv', 'utf8')); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename=invoice.csv'); res.send(csvString);Then clean up the uploaded file with
fs.unlinkSync(req.file.path). - Test the API
Start the server with
node index.js. Use a tool like Postman or curl to send a POST request with the PDF file as form-data under the field nameinvoice. You should get a CSV file in response.
Common Mistakes
- Assuming all invoices have the same layout – Different vendors use different formats. Your regex patterns may work for one but fail for another. Always test with multiple samples.
- Ignoring multi-line line items – Descriptions can wrap across lines. Your parsing needs to handle that, perhaps by joining lines until a new quantity is found.
- Not handling currency formats – Amounts may have commas or spaces as thousand separators. Strip them before converting to numbers.
- Forgetting to delete uploaded files – Leaving files in the uploads folder can clutter your server and cause disk space issues.
- Using synchronous file operations in async routes – This can block the event loop. Use async versions or wrap them in promises.
Tips
- Use a library like pdf-parse with a fallback – Some PDFs are scanned images, not text. For those, you’ll need OCR (like Tesseract.js), but that’s beyond this guide.
- Normalize date formats – Convert all dates to ISO format (YYYY-MM-DD) for easier sorting and analysis.
- Validate the extracted data – Check that required fields exist and that totals are positive. If something is missing, return an error rather than a wrong CSV.
- Add logging – Log the raw text and extracted data to a file for debugging when something goes wrong.
- Consider using a PDF table extraction library – If your invoices have tabular data, libraries like
pdf-table-extractormight be more accurate than manual regex.
Troubleshooting
- PDF text is empty or gibberish – This usually means the PDF is image-based. You’ll need OCR. Alternatively, check if the PDF is encrypted; if so, you’ll need to decrypt it first.
- Regex matches nothing – Print the raw text to the console and inspect it. The format may be different than expected (e.g., “Invoice No:� instead of “Invoice #�).
- CSV has extra commas or quotes – Use the csv-writer’s quoting option or escape characters manually. Ensure descriptions with commas are enclosed in double quotes.
- Server crashes on large PDFs – Increase the upload size limit in Multer:
multer({ limits: { fileSize: 10 * 1024 * 1024 } }). - Port already in use – Change the port in
app.listenor kill the process using the port.
Frequently Asked Questions
Can I convert multiple PDFs at once?
Yes, modify the route to accept multiple files (using upload.array('invoices')) and loop through each, returning a combined CSV or a zip.
What if the invoice has no line items?
You can still extract the header fields (invoice number, date, total) and output a single row with empty item fields.
Is OCR necessary for scanned invoices?
Yes, if the PDF is a scanned image, you need OCR. Tesseract.js can be integrated, but it’s slower and less accurate than text-based extraction.
How do I handle different currencies?
You can add a currency column and detect the symbol from the text. For conversion, you’d need an external API, but that’s beyond this guide.
Can I deploy this API to a cloud platform?
Yes, it’s a standard Node.js app. Deploy to Heroku, AWS, or any service that supports Node.js. Ensure you set up environment variables for any secrets.
What if the PDF is password-protected?
pdf-parse might fail. You’ll need to remove the password first using a tool like qpdf or a library like pdf-lib before parsing.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- 3 OS Updates Meaning: Sony’s 3-Year Promise
- Robot Training Data: 8 Questions to Ask
- Compare Refrigerator Energy Use & Running Costs
- Apple Watch Remote iTunes: Quick Guide
- Best Alternatives to Optical Drives
- Beats Solo 4 50 Hour Battery But No Noise?2.00 %/h