Learn Creative Coding (#80) - Working with APIs

Last episode we talked about data as creative material -- the idea that numbers from the real world can drive visual decisions instead of our imagination alone. We mapped temperature to position, wind speed to line length, step counts to ring thickness. The data became the artist. But here's the thing: all that data was fake. We typed arrays of numbers into our code by hand. const rainfall = [42, 38, 51, ...] -- those weren't real rainfall numbers. We invented them.
Real data lives out there on the internet, behind URLs, waiting to be fetched. Weather services, space agencies, earthquake monitors, city transit systems -- they all expose their data through APIs. An API (Application Programming Interface) is just a URL that returns structured data instead of a web page. You ask it a question ("what's the weather in Antwerp?") and it answers with JSON -- a block of neatly organized numbers and strings your code can parse and use.
This episode is the bridge between the concept of data art and actually making it with real-world data. We need to learn how the browser fetches external data, how to handle the things that go wrong (because they will), and how to structure our code so we're not hammering some poor server 60 times per second in our animation loop.
fetch() -- the browser's data pipeline
The fetch() function is built into every modern browser. You give it a URL, it makes an HTTP request to that URL, and it returns the response. The catch: network requests take time. Your code can't freeze and wait -- JavaScript is single-threaded, and blocking the thread would freeze your canvas. So fetch() is asynchronous. It returns a Promise, which resolves when the data arrives.
Two ways to write it. The older Promise chain style:
fetch('https://api.open-meteo.com/v1/forecast?latitude=51.22&longitude=4.40¤t_weather=true')
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data.current_weather.temperature);
console.log(data.current_weather.windspeed);
});
And the newer async/await style, which reads more like normal sequential code:
async function getWeather() {
const response = await fetch(
'https://api.open-meteo.com/v1/forecast?latitude=51.22&longitude=4.40¤t_weather=true'
);
const data = await response.json();
console.log(data.current_weather.temperature);
console.log(data.current_weather.windspeed);
}
getWeather();
Both do the same thing. I prefer async/await because it's easier to read and debug -- the code flows top to bottom without nesting. The await keyword pauses execution of that function (not the whole thread) until the Promise resolves. The rest of your page keeps running, your canvas keeps drawing.
That URL is real, by the way. Open-Meteo is a free weather API that requires no API key. The latitude/longitude is Antwerp. You can paste that URL into your browser and see the raw JSON response. Try it -- looking at the raw data helps you understand the structure before writing code to parse it.
JSON: the shape of API data
Almost every API returns JSON (JavaScript Object Notation). It's nested objects and arrays -- curly braces for objects, square brackets for arrays, key-value pairs separated by colons. Here's what the Open-Meteo response looks like:
{
"latitude": 51.22,
"longitude": 4.4,
"current_weather": {
"temperature": 18.2,
"windspeed": 12.4,
"winddirection": 230,
"weathercode": 3,
"time": "2026-05-31T14:00"
}
}
To access nested values you chain dot notation: data.current_weather.temperature gives you 18.2. If a property contains an array, you index into it: data.hourly.temperature_2m[0] would give the first hourly temperature.
The critical habit: always console.log(data) first. Expand the object in your browser's console. See what's actually there before writing code that assumes a specific structure. APIs evolve, properties get renamed, nesting changes. Logging first saves you from staring at "undefined" wondering what went wrong.
The cardinal rule: fetch once, draw many
This is the mistake I see most often when people combine APIs with creative coding. They put the fetch call inside their draw loop:
// DO NOT DO THIS
function draw() {
fetch('https://api.example.com/data')
.then(r => r.json())
.then(data => {
// draw something with data
});
requestAnimationFrame(draw);
}
That fires 60 HTTP requests per second. The API server will hate you, probably rate-limit you within seconds, and your animation will stutter because each frame waits for a network round-trip. APIs are not frame-rate data sources.
The correct pattern: fetch the data once (or on a timer), store it in a variable, and let your draw loop read from that variable:
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let weatherData = null;
async function fetchData() {
const response = await fetch(
'https://api.open-meteo.com/v1/forecast?latitude=51.22&longitude=4.40&hourly=temperature_2m&forecast_days=1'
);
const data = await response.json();
weatherData = data.hourly.temperature_2m; // array of 24 temps
}
function draw() {
ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, 800, 400);
if (weatherData) {
for (let i = 0; i < weatherData.length; i++) {
const x = (i / 23) * 740 + 30;
const temp = weatherData[i];
const y = 380 - ((temp + 5) / 40) * 360;
const hue = ((temp + 5) / 35) * 240;
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${240 - hue}, 60%, 50%)`;
ctx.fill();
}
} else {
ctx.fillStyle = '#666';
ctx.font = '14px monospace';
ctx.fillText('fetching weather data...', 20, 30);
}
requestAnimationFrame(draw);
}
fetchData();
draw();
The fetch runs once. The draw loop runs forever, reading from weatherData. If the data hasn't arrived yet, we show a loading message. Once it arrives, we draw 24 hourly temperature points as colored circles -- cold-blue on the left transitioning to warm-red for hotter hours. Clean separation between data acquisition and visualization.
If you want the data to refresh (say, every 30 minutes for a live installation), use setInterval:
fetchData(); // fetch immediately
setInterval(fetchData, 1800000); // then every 30 minutes
CORS: the wall you'll hit
You're going to try fetching from some API, and instead of data you'll get a red error in the console: "Access to fetch at 'X' from origin 'Y' has been blocked by CORS policy." Welcome to CORS (Cross-Origin Resource Sharing). It's a browser security feature that prevents web pages from making requests to domains different from their own -- unless the target server explicitly allows it.
Open-Meteo works fine because their server sends the header Access-Control-Allow-Origin: *, meaning any page can access it. But many APIs don't set this header, and the browser blocks the request. It's not a bug, it's deliberate protection.
Your options when you hit CORS:
- Use APIs that support CORS -- Open-Meteo, JSONPlaceholder, many NASA APIs, most APIs explicitly designed for browser use
- Use a CORS proxy -- services like
https://corsproxy.io/?prepend to your URL and relay the request server-side, adding the required headers. Fine for prototyping but don't rely on it for production - Fetch server-side -- if you have a Node.js server, fetch from there (no CORS in server-to-server requests) and serve the data to your frontend
For creative coding sketches and prototypes, stick to CORS-friendly APIs. There are plently of them and they cover most of what you'd want for data art.
Error handling: networks break
Fetch can fail. The server might be down, your internet might blip, the API might return a 500 error. If you don't handle these cases, your sketch crashes silently -- the data variable stays null forever and your visualization is blank with no explanation.
async function fetchDataSafe() {
try {
const response = await fetch(
'https://api.open-meteo.com/v1/forecast?latitude=51.22&longitude=4.40&hourly=temperature_2m&forecast_days=1'
);
if (!response.ok) {
console.error('API error:', response.status, response.statusText);
return null;
}
const data = await response.json();
return data.hourly.temperature_2m;
} catch (err) {
console.error('fetch failed:', err.message);
return null;
}
}
response.ok is true for status codes 200-299 and false for everything else (404, 500, etc.). The try/catch handles network failures (no internet, DNS errors, timeouts). Always return something predictable -- null, an empty array, fallback data. Your draw loop needs to know what to do when data isn't available.
A nice pattern for creative coding: generate fallback data that's plausible enough to sketch with while the real data loads or if it fails entirely:
function generateFallbackWeather() {
const temps = [];
for (let i = 0; i < 24; i++) {
// sinusoidal fake temperature: cold at night, warm at midday
temps.push(10 + Math.sin((i - 6) / 24 * Math.PI * 2) * 8 + (Math.random() - 0.5) * 3);
}
return temps;
}
async function loadWeather() {
const data = await fetchDataSafe();
weatherData = data || generateFallbackWeather();
}
Now your sketch always has something to draw, even offline. The visual pattern won't be "real" without the API connection, but you can still develop and test your mapping logic.
Multiple API calls: combining data sources
One dataset is interesting. Two datasets combined are more interesting. Temperature AND wind speed from the same API, or weather from one API combined with ISS position from another. Promise.all fetches multiple URLs in parallel:
async function fetchMultiple() {
const [weatherRes, issRes] = await Promise.all([
fetch('https://api.open-meteo.com/v1/forecast?latitude=51.22&longitude=4.40¤t_weather=true'),
fetch('http://api.open-notify.org/iss-now.json')
]);
const weather = await weatherRes.json();
const iss = await issRes.json();
console.log('Antwerp temp:', weather.current_weather.temperature);
console.log('ISS position:', iss.iss_position.latitude, iss.iss_position.longitude);
}
Both requests launch simultaneously. Promise.all resolves when ALL of them complete. This is faster than fetching sequentially (one after another) because network requests overlap. If any single request fails, the whole Promise.all rejects -- so wrap it in try/catch and handle partial failure if that matters for your sketch.
For data art, combining sources creates unexpected juxtapositions. What if the ISS's longitude controls the hue of your particle system and Antwerp's wind speed controls particle velocity? Totaly arbitrary mapping, but the result is a visual that's driven by two unrelated real-world phenomena simultaneously. That's the kind of conceptual layering that makes data art interesting.
Pagination: when data comes in pages
Some APIs don't give you everything at once. If you ask for all earthquakes this month, there might be thousands -- too many for one response. Instead they paginate: give you 20 results plus a link to the next page. You have to fetch page by page.
async function fetchAllPages(baseUrl) {
let allResults = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}&page=${page}&limit=100`);
const data = await response.json();
allResults = allResults.concat(data.results);
if (data.results.length < 100) {
hasMore = false; // less than full page = last page
} else {
page++;
}
}
return allResults;
}
The pattern varies by API -- some use page and limit, some use offset, some include a next URL in the response. Check the API docs. The principle is the same: keep fetching until you've accumulated all the data you need.
For creative coding you often don't need ALL the data. 100 earthquakes is enough to make a compelling visualization. Don't paginate through 50 pages of historical data if the first page gives you plenty of material. Be respectful of API servers -- they're often run by small teams or individuals.
Rate limiting: don't be rude
Free APIs limit how many requests you can make. OpenWeatherMap allows 60 per minute on their free tier. NASA's APOD API allows 30 per hour without a key. If you exceed the limit, you get a 429 (Too Many Requests) response and might be temporarily blocked.
For creative coding this rarely matters if you follow the "fetch once, draw many" pattern. One request at page load, maybe one refresh every 30 minutes. But if you're iterating quickly during development -- saving your file, auto-reloading, triggering a fresh fetch every time -- you can accidentally hit limits fast.
Simple mitigation: cache the response in localStorage:
async function fetchWithCache(url, maxAge) {
const cacheKey = 'api_cache_' + url;
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < maxAge) {
return data; // still fresh
}
}
// cache miss or expired -- fetch fresh
const response = await fetch(url);
const data = await response.json();
localStorage.setItem(cacheKey, JSON.stringify({
data: data,
timestamp: Date.now()
}));
return data;
}
// use with 30 minute cache
const weather = await fetchWithCache(
'https://api.open-meteo.com/v1/forecast?latitude=51.22&longitude=4.40¤t_weather=true',
30 * 60 * 1000
);
Now rapid page reloads during development won't hit the API repeatedly -- they'll read from the localStorage cache until it expires. The API only gets hit once per 30 minutes regardless of how often you reload.
API keys: authentication
Some APIs require a key -- a unique string that identifies you. You sign up on their website, get a key, and include it in your request URL or headers. OpenWeatherMap, Google Maps, Spotify, Twitter -- all require keys.
For creative coding sketches that only run locally, putting the key in your JavaScript is technically fine (only you see the source). But if you ever publish the page or push it to GitHub, the key is exposed. Anyone can grab it from your source code and use your quota.
Better approaches:
- Use APIs that don't require keys (Open-Meteo, JSONPlaceholder, many NASA endpoints)
- If you must use a keyed API, keep the key in a
.envfile and fetch via your own backend that injects the key server-side - For published creative coding work, generate the data once, save it as a static JSON file, and load that file instead of calling the API live
For this series we'll stick with keyless APIs. There are enough of them to build anything we need.
Creative exercise: global weather portrait
Allez, let's build the real thing. A live weather portrait -- fetch current conditions for 10 cities around the world and turn each city into a visual element. Temperature maps to circle radius, wind speed maps to rotation speed of a ring around the circle, and weather code (clear, cloudy, rain, etc.) maps to color.
const canvas = document.createElement('canvas');
canvas.width = 900;
canvas.height = 500;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const cities = [
{ name: 'Antwerp', lat: 51.22, lon: 4.40 },
{ name: 'Tokyo', lat: 35.68, lon: 139.69 },
{ name: 'New York', lat: 40.71, lon: -74.01 },
{ name: 'Sydney', lat: -33.87, lon: 151.21 },
{ name: 'Cairo', lat: 30.04, lon: 31.24 },
{ name: 'Reykjavik', lat: 64.15, lon: -21.94 },
{ name: 'Mumbai', lat: 19.08, lon: 72.88 },
{ name: 'Lima', lat: -12.05, lon: -77.04 },
{ name: 'Nairobi', lat: -1.29, lon: 36.82 },
{ name: 'Moscow', lat: 55.76, lon: 37.62 }
];
let cityWeather = null;
async function fetchAllWeather() {
const promises = cities.map(city =>
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${city.lat}&longitude=${city.lon}¤t_weather=true`)
.then(r => r.json())
.then(data => ({
name: city.name,
temp: data.current_weather.temperature,
wind: data.current_weather.windspeed,
code: data.current_weather.weathercode
}))
.catch(() => ({
name: city.name,
temp: 15,
wind: 5,
code: 0
}))
);
cityWeather = await Promise.all(promises);
}
function weatherColor(code) {
// WMO weather codes
if (code <= 1) return { h: 45, s: 70, l: 55 }; // clear: warm gold
if (code <= 3) return { h: 210, s: 30, l: 60 }; // cloudy: cool gray-blue
if (code <= 67) return { h: 220, s: 55, l: 45 }; // rain/drizzle: blue
if (code <= 77) return { h: 190, s: 40, l: 70 }; // snow: ice blue
return { h: 270, s: 50, l: 40 }; // thunderstorm: purple
}
let time = 0;
function draw() {
ctx.fillStyle = '#080812';
ctx.fillRect(0, 0, 900, 500);
if (!cityWeather) {
ctx.fillStyle = '#555';
ctx.font = '14px monospace';
ctx.fillText('fetching weather for 10 cities...', 20, 30);
requestAnimationFrame(draw);
return;
}
time += 0.016;
for (let i = 0; i < cityWeather.length; i++) {
const cw = cityWeather[i];
const col = i % 5;
const row = Math.floor(i / 5);
const x = 90 + col * 175;
const y = 140 + row * 220;
// radius from temperature: -10C = 15px, 40C = 55px
const radius = 15 + ((cw.temp + 10) / 50) * 40;
// color from weather code
const color = weatherColor(cw.code);
// main circle
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${color.h}, ${color.s}%, ${color.l}%, 0.6)`;
ctx.fill();
// rotating ring: speed from wind
const ringRadius = radius + 12;
const rotationSpeed = cw.wind * 0.02;
const segments = 8;
ctx.save();
ctx.translate(x, y);
ctx.rotate(time * rotationSpeed);
for (let s = 0; s < segments; s++) {
const a = (s / segments) * Math.PI * 2;
const arcLen = 0.3;
ctx.beginPath();
ctx.arc(0, 0, ringRadius, a, a + arcLen);
ctx.strokeStyle = `hsla(${color.h}, ${color.s}%, ${color.l + 15}%, 0.5)`;
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.restore();
// city name and temp
ctx.fillStyle = 'rgba(200, 200, 220, 0.7)';
ctx.font = '11px monospace';
ctx.textAlign = 'center';
ctx.fillText(cw.name, x, y + radius + 28);
ctx.fillText(`${cw.temp.toFixed(1)}C ${cw.wind.toFixed(0)}km/h`, x, y + radius + 42);
}
requestAnimationFrame(draw);
}
fetchAllWeather();
draw();
Ten cities, ten circles. The hot cities (Cairo, Mumbai) have large circles. The cold ones (Reykjavik, Moscow in winter) have small circles. Rainy cities glow blue, clear ones glow gold. The wind-driven rings spin faster for windy cities and barely move for calm ones. It's a real-time portrait of global weather conditions rendered as an abstract composition.
Every time you reload the page, it fetches fresh data. If it's nighttime in Antwerp, the temperature drops and the circle shrinks. If a storm hits Tokyo, the color shifts to purple and the ring spins faster. The visualization is alive -- connected to the actual state of the world right now.
Real-time data: WebSockets and Server-Sent Events
The fetch approach is request-response: you ask, you receive, done. But some data streams continuously -- stock prices ticking every second, chat messages arriving unpredictably, IoT sensors reporting every 200 milliseconds. For these you need a persistent connection.
Server-Sent Events (SSE) are the simpler option. The server pushes text messages to you over a long-lived HTTP connection:
const eventSource = new EventSource('https://stream.example.com/prices');
eventSource.onmessage = function (event) {
const data = JSON.parse(event.data);
// data arrives as it's pushed -- no polling needed
addDataPoint(data.price);
};
eventSource.onerror = function () {
console.log('stream disconnected, will auto-reconnect');
};
SSE is one-directional (server to client) and auto-reconnects on failure. Good for price feeds, notification streams, live sensor data.
WebSockets are bidirectional. You and the server talk back and forth in real time:
const ws = new WebSocket('wss://stream.example.com/live');
ws.onopen = function () {
ws.send(JSON.stringify({ subscribe: 'temperature' }));
};
ws.onmessage = function (event) {
const data = JSON.parse(event.data);
addDataPoint(data.value);
};
For creative coding with streaming data, the challenge is buffering. You can't draw every single incoming data point as a separate visual element -- they arrive too fast. Instead, accumulate points into a buffer and let the draw loop consume from the buffer at its own pace:
const buffer = [];
const maxBuffer = 200;
// data arrives asynchronously
function addDataPoint(value) {
buffer.push(value);
if (buffer.length > maxBuffer) buffer.shift();
}
// draw loop reads from buffer
function draw() {
ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, 800, 400);
for (let i = 0; i < buffer.length; i++) {
const x = (i / maxBuffer) * 780 + 10;
const y = 380 - (buffer[i] / 100) * 360;
ctx.fillStyle = `hsla(${buffer[i] * 2}, 60%, 50%, 0.7)`;
ctx.fillRect(x, y, 3, 3);
}
requestAnimationFrame(draw);
}
The buffer acts as a sliding window. New data pushes old data out. The draw loop always shows the most recent 200 points. The visual scrolls left as new data arrives. This is how you'd visualize a live audio stream, a real-time heartbeat sensor, or a cryptocurrency price ticker as creative coding material.
What's next
We can fetch data now. We know how to call APIs, handle errors, cache results, and keep our draw loops clean. But raw API responses are just one data format. Real-world datasets also come as CSV files, GeoJSON coordinates, XML feeds, and other structured formats that need parsing before you can map them to visuals. The next step is learning to crack open those file formats and extract the numbers hiding inside.
't Komt erop neer...
fetch()is the browser's built-in tool for grabbing data from URLs. It's asynchronous -- returns a Promise that resolves when the data arrives. Useasync/awaitfor clean, readable code. The data doesn't block your canvas from drawing while it loads- APIs return JSON: nested objects and arrays accessible via dot notation (
data.current_weather.temperature). Alwaysconsole.log(data)first to inspect the structure before writing parsing code. APIs change, properties get renamed -- log first, code second - The cardinal rule: fetch once, draw many. Never put fetch() inside your animation loop. Fetch the data once (or on a timed interval), store it in a variable, and let the draw loop read from that variable. Anything else hammers the server and stutters your animation
- CORS blocks cross-origin requests unless the target server allows it. Use CORS-friendly APIs (Open-Meteo, JSONPlaceholder, NASA) for browser-based creative coding. If you hit a CORS wall, try a different API before reaching for proxy hacks
- Error handling: wrap fetch in try/catch, check
response.ok, and provide fallback data. Networks fail. APIs go down. Your sketch should degrade gracefully -- show generated placeholder data while real data is unavailable Promise.allfetches multiple APIs in parallel. Combine data sources for richer visualizations -- weather from one API, satellite positions from another. Arbitrary cross-source mappings create unexpected conceptual juxtapositions- Rate limits exist on free APIs. Cache responses in
localStorageto avoid hitting limits during rapid development reloads. One fetch every 30 minutes is plenty for most data art installations - For streaming data (live prices, sensor feeds, chat), use Server-Sent Events or WebSockets. Buffer incoming data in an array and let the draw loop consume at its own pace. The buffer is a sliding window -- new data pushes old data out
- The creative exercise: a global weather portrait. Ten cities fetched in parallel from Open-Meteo. Temperature maps to circle size, weather code to color, wind speed to ring rotation. The result is a live abstract composition reflecting actual global conditions right now
Sallukes! Thanks for reading.
X