(Draft) What I've learned building VSCarbon

Published at Aug 21, 2025

Learning by building is exciting in the way that I figure things out as I go, feeling like I’m on a quest as each level requires new skills.

Full development cycle

Design pattern

  • Adapter. When interest arises, there’s a need to extend VSCarbon to EU countries. It means I have to integrate other APIs (e.g., Electricity Maps, Carbon Aware Computing), creating a new strutural challenge. Luckily, someone recommended adapter pattern. Instead of rewriting core logic for each provider, I can abstract data layer (this feels deja vu because a CTO mentioned abstraction as a way to deal with business diversity in a job interview).
  • I was worried it’s gonna be heavy for a VS Code extension because it sounds like big enterprise-y layers, but in my case, it looks less like design pattern boilerplate and more like a simple translation function per API.

Anw, I can define a common interface like this

type CarbonData = {
	timestamp: string
	region: string
	carbonIntensity: number // gCO2/kWh
	generationMix?: Record<string, number> // sadly some APIs don't provide energy mix
}

Adapters here are functions that normalise responses. I can add more providers later without changing much UI/logic. So, it’s a cleaner way to avoid if (provider === “UK”) else if (“EU”) everywhere

// National Grid adapter for UK users
export async function fetchUKCarbonData(): Promise<CarbonData[]> {
	const res = await fetch('https://api.carbonintensity.org.uk/regional')
	const json = await res.json()

	return json.data.map((region: any) => ({
		timestamp: region.from,
		region: region.shortname,
		carbonIntensity: region.intensity.actual,
		generationMix: region.generationmix.reduce(
			(acc: any, g: any) => ({ ...acc, [g.fuel]: g.perc }),
			{}
		)
	}))
}

// Otherwise Electricity Maps adapter for EU/global users
export async function fetchEUCarbonData(countryCode: string): Promise<CarbonData> {
	const res = await fetch(
		`https://api.electricitymap.org/v3/carbon-intensity/latest?zone=${countryCode}`,
		{ headers: { 'auth-token': process.env.ELECTRICITYMAP_TOKEN! } }
	)
	const json = await res.json()

	return {
		timestamp: json.datetime,
		region: json.zone,
		carbonIntensity: json.carbonIntensity,
		generationMix: json.productionMix
	}
}

Transforming and normalising data

My tool integrates data from two distinct APIs - the UK’s National Grid Carbon Intensity API and the Electricity Maps API. Each provides carbon intensity data but with different structures, requiring transformation and normalisation to present a unified interface.

UK National Grid API returns nested regional data:

{
  "data": [{
    "shortname": "England",
    "data": [{
      "intensity": {
        "forecast": 245,
        "index": "moderate"
      },
      "generationmix": [
        { "fuel": "gas", "perc": 40.2 },
        { "fuel": "wind", "perc": 25.1 }
      ]
    }]
  }]
}

Electricity Maps API uses a flatter structure:

{
  "zone": "GB",
  "carbonIntensity": 245,
  "datetime": "2024-01-15T10:00:00.000Z",
  "powerConsumptionBreakdown": {
    "gas": 15420,
    "wind": 9630,
    "solar": 2100
  }
}

1. Unified Data Schema

So I defined a common interface CarbonData that both adapters transform to

interface CarbonData {
  intensity: number        // gCO₂/kWh
  index?: CarbonIntensityIndex
  region: string
  timestamp: Date
  mix?: GenerationMix[]
  source: DataSource
}

2. Adapter Pattern Implementation

Each API gets its own adapter with transformation logic:

UK Adapter

const transformNationalGridResponse = (response: NationalGridResponse): CarbonData => {
  const regionalData = response.data[0]
  const intensityData = regionalData.data[0]

  return {
    intensity: intensityData.intensity.forecast,
    index: intensityData.intensity.index as CarbonIntensityIndex,
    region: regionalData.shortname || "England",
    timestamp: new Date(),
    mix: intensityData.generationmix?.map((item) => ({
      fuel: item.fuel,
      perc: item.perc,
    })),
    source: "national-grid",
  }
}

EU Adapter

// Transform power consumption to percentages
const calculatePercentages = (powerBreakdown: Record<string, number>) => {
  const totalConsumption = Object.values(powerBreakdown)
    .filter(value => value > 0)
    .reduce((sum, value) => sum + value, 0)

  const percentages: Record<string, number> = {}
  for (const [fuelType, value] of Object.entries(powerBreakdown)) {
    if (value > 0) {
      percentages[fuelType] = (value / totalConsumption) * 100
    }
  }
  return percentages
}

// Map Electricity Maps API  fuel types to our standard
const fuelTypeMapping = {
  nuclear: "nuclear",
  wind: "wind", 
  solar: "solar",
  gas: "gas",
  coal: "coal",
  "hydro discharge": "hydro",
  biomass: "biomass"
}
const groupedByFuel: Record<string, number> = {}
for (const [fuelType, percentage] of Object.entries(percentages)) {
  const mappedFuel = fuelTypeMapping[fuelType.toLowerCase()] || "other"
  groupedByFuel[mappedFuel] = (groupedByFuel[mappedFuel] || 0) + percentage
}

Privacy

Instead of detecting user location (which may be inaccurate due to devs using VPN), I let users input their regional postcode (more private and practical as well)

Performance, SEO

Writing

Promoting

and I’m grateful that VSCarbon is receiving interest and support beyond the UK.