Aral Balkan

Mastodon icon RSS feed icon

Scraping the latest EU VAT rates for e-services from the European Commission’s web site with Node.js

So, you now know how to verify an EU VAT number with Node.js.

Lucky you! (Don’t say I don’t spoil you.)

But do you know what the latest VAT rates are every EU country?

(Do you? Huh, do you, punk?)

OK, so you can find them here. Yes, that’s an HTML page. No, I haven’t been able to find a simple JSON feed or anything. Yes, it is 2021.


So anyway, here’s one way you can scrape it using Node.js:1

  1. Install the dependencies.

    npm install cheerio node-fetch
  2. Scrape it like you mean it.

    import * as cheerio from 'cheerio'
    import fetch from 'node-fetch'
    const htmlLikeIts1999 = await (await fetch('')).text()
    const $ = cheerio.load(htmlLikeIts1999)
    const taxRates = {}
    $('tr.table-vat-rates').each((index, row) => {
      const cells = $(row).find('td')
      const countryName = cells.first().html()
      const taxRate = parseInt(cells.last().html().replace(/\<br.*?$/, ''))
      taxRates[countryName] = taxRate

When you run it, you should see a nice hash table of the latest VAT rates resembling the one below appear in your console.

  Austria: 20,
  Belgium: 21,
  Bulgaria: 20,
  Cyprus: 19,
  Sweden: 25,
  Slovenia: 22,
  Slovakia: 20

So, there you go.

The JSON endpoint for EU VAT rates that you never knew you wanted is now yours to cherish.

(Until they change the HTML structure, that is.)

w00t, etc.

Like this? Fund us!

Small Technology Foundation is a tiny, independent not-for-profit.

We exist in part thanks to patronage by people like you. If you share our vision and want to support our work, please become a patron or donate to us today and help us continue to exist.

  1. You feeling lucky? If you’re a real daredevil, why not use regular expressions instead? Go on… I double dare you…

    import fetch from 'node-fetch'
    const htmlLikeIts1999 = await (await fetch('')).text()
    const taxRates = {}
    htmlLikeIts1999.split('\n').filter(line => line.includes('<tr class=')).map(row => row.trim().replace(/^\<tr.*?\>/, '').split('td')).forEach(row => taxRates[row[1].replace('>', '').replace('</', '')] = parseInt(row[7].replace(/<br.*?$/, '').replace('>', '')))

    (It’s ok, you can cry a little now.) ↩︎