(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.