Programs That Listen
Building Things That Actually Do Things
A chess player stares across the board. They don’t play all their moves at once and leave. They make a move, then wait. They watch. They respond to what you do, adjusting their strategy with every piece you touch. The game exists in the space between two minds, and neither player knows in advance where it will end.
A vending machine stands in a hallway, doing nothing. It could stand there for hours, motionless and content. Then you press a button, and it whirs to life: checking your selection, verifying your payment, dispensing your questionable snack. It responds to you. Without your input, it is furniture.
A good conversation goes somewhere neither person expected. You mention a book, they mention a trip, and twenty minutes later you’re both passionately debating whether octopuses dream. Nobody planned this. It emerged from two parties listening and responding, each building on what came before.
Everything you’ve learned so far has been building toward this. Variables, functions, data structures, debugging, libraries. These are the parts. Now we assemble them into something that does something. Programs that listen to users, talk to the internet, and behave like the software you actually use every day.
Programs That Run Once vs. Programs That Keep Running
Most programs written so far have had a clear arc: they start, they compute, they display a result, they exit. Like a firework. Briefly impressive, then gone.
But think about the software you use daily. Your web browser doesn’t load one page and quit. A game doesn’t render one frame and close. A chat app doesn’t send one message and shut down. These programs persist. They wait for you to do something, respond, then wait again.
The difference is a loop. That’s it. The fundamental architecture of every interactive program (every game, every app, every tool you’ve ever used) is a loop that listens and responds, patient as a cat at a closed door.
The Main Loop, Revisited
You encountered this pattern in an earlier chapter. It bears repeating, because it is the single most important pattern in all of interactive software:
- Wait for something to happen
- Respond to what happened
- Go back to step 1
Your phone’s home screen does this. Spotify does this. The ATM at the bank does this. The elevator does this. The pattern is universal, and once you see it, you cannot unsee it.
A Tiny Text Adventure
Here’s a small exploration game that demonstrates the pattern. It has state (where you are), input (your commands), output (what you see), and a loop that keeps the conversation going:
const readline = require("node:readline/promises");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
async function play() {
let location = "entrance";
while (true) {
if (location === "entrance") {
console.log("\nYou are at the entrance of a cave.");
console.log("You can go: north");
} else if (location === "cave") {
console.log("\nYou are inside a dark cave.");
console.log("You can go: south");
console.log("There is a treasure here!");
}
const command = await rl.question("\nWhat do you do? ");
if (command === "north" && location === "entrance") {
location = "cave";
console.log("You venture into the darkness...");
} else if (command === "south" && location === "cave") {
location = "entrance";
console.log("You return to the entrance.");
} else if (command === "quit") {
console.log("Thanks for playing!");
break;
} else {
console.log("You can't do that here.");
}
}
rl.close();
}
play();
This program is a conversation. You tell it what to do, it tells you what happened, and you respond to that. The location variable is its memory. The while (true) loop is its willingness to keep listening. The break on “quit” is its understanding that conversations end.
If you’re thinking “wait, so every video game is fundamentally just a fancier version of this,” you’re correct. More state, faster loops, better graphics. But the pattern (input, process, output, repeat) is identical. World of Warcraft is this cave with better lighting.
Why This Pattern Matters
Every application you will ever build, from a humble calculator to a sprawling web service, runs some version of this loop. A web server waits for requests, processes them, sends responses, and waits again. A mobile app waits for taps, responds to them, redraws the screen, and waits again. Different costumes, same heartbeat.
Understanding this pattern means understanding how all interactive software works. The rest is detail.
Talking to the Internet: APIs
Your programs so far have lived in isolation. They take input from a keyboard and display output on a screen. They know nothing of the wider world. They are, in a sense, solitary creatures: hermit crabs of the computational ecosystem.
Real programs talk to each other. Your weather app talks to a weather service. Your email client talks to a mail server. Your music player talks to a streaming service. These conversations happen through APIs.
What’s an API?
API stands for Application Programming Interface, which is one of those terms that sounds more complicated than it is. An API is simply a way for one program to talk to another. It’s the agreed-upon format for the conversation: what to ask for, how to ask, and what the response will look like.
Think of it like ordering at a restaurant. You don’t walk into the kitchen and start cooking. You don’t need to know how the kitchen works. You look at the menu (the API documentation), place your order in the expected format (“I’ll have the soup, please”), and receive a response (soup, ideally). The menu is the interface between you and the kitchen.
Web APIs work the same way. Your program sends a request (usually to a URL) and gets back data, typically in JSON format. You’ve already worked with JSON. This is where that knowledge pays off.
Making a Request
Remember fetch from the previous chapter? This is where it becomes genuinely useful. Fetching data from a web API looks something like this:
const response = await fetch("https://api.example.com/weather?city=London");
const data = await response.json();
console.log(data.temperature);
console.log(data.conditions);
Two lines to reach across the internet, ask a server a question, and receive an answer, the whole exchange done before you’ve finished reading it. No import needed: fetch() is built into JavaScript. It sends a request to the URL and returns a promise, which await pauses on until the answer arrives. The .json() method parses the response into a JavaScript object, the same data structure you learned in an earlier chapter. Then you access the data by key, just as you would with any object.
What Comes Back: JSON in the Wild
A real API response is just JSON: data structured as objects and arrays, the same structures you’ve already learned to work with:
{
"city": "London",
"temperature": 12,
"unit": "celsius",
"conditions": "overcast",
"humidity": 78,
"wind": {
"speed": 15,
"direction": "southwest"
},
"forecast": [
{"day": "Tuesday", "high": 14, "low": 9},
{"day": "Wednesday", "high": 11, "low": 7}
]
}
This is an object containing strings, numbers, a nested object (wind), and an array of objects (forecast). Nothing here is new. You’ve been reading this structure since the data structures chapter. The only difference is that this data came from a server thousands of miles away, carried to your program on beams of light through fiber-optic cables, rather than being typed into your source code by hand.
When Things Go Wrong
The internet is unreliable. Servers go down. Connections time out. APIs change their response format without warning, like a restaurant that reorganizes its menu while you’re ordering. Your code needs to handle this gracefully.
try {
const response = await fetch("https://api.example.com/weather?city=London");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`); // fetch won't throw for you
}
const data = await response.json();
console.log(`Temperature: ${data.temperature}°C`);
} catch (error) {
console.log("Could not reach the weather service:", error.message);
}
The try/catch pattern should look familiar from the debugging chapter. Unlike some other languages, fetch() doesn’t throw just because the server responded with an error status like 404 or 500, you have to check response.ok and throw it yourself, as this code does. It does throw (a TypeError) when the network fails outright: no connection, DNS failure, and so on. What it never catches is missing data. If the response is missing a temperature field, data.temperature doesn’t raise anything. It quietly returns undefined, and you get Temperature: undefined°C printed to the screen, a bug that surfaces far from its cause. Without the checks this code does have, your program would crash outright on a network failure. With them, it fails gracefully, like a waiter who says “I’m sorry, the kitchen is closed” instead of throwing a plate at the wall.
APIs are everywhere. Every time you check the weather on your phone, scroll social media, or search for a restaurant, your app is making API calls behind the scenes. Dozens of them. Sometimes hundreds. The modern internet is a vast, ceaselessly chattering network of programs talking to programs, exchanging JSON with the urgency of traders on a stock exchange floor.
Building Your First Real Project
This is the part where everything comes together.
You’re going to build a command-line weather checker. It’s small, maybe forty lines when finished, the sort of thing you could read in the time it takes a kettle to boil. But those forty lines lean on nearly every skill you’ve developed across this course: decomposing problems, reading code, working with data structures, handling errors, using libraries, and collaborating with AI.
This isn’t a toy exercise. It’s the same process professional developers follow when building features. The scale is different. The method is identical.
Step 1: Define What You Want
Before touching any code, before opening any editor, before prompting any AI. Think. This is the part where a carpenter stands with the pencil and the tape measure, the saw still hanging on the wall. What does this program actually need to do?
This is the decomposition skill from the very first chapters. Break the problem into pieces:
- Ask the user for a city name
- Send that city name to a weather API
- Receive weather data back
- Display the weather in a readable format
- Handle errors if something goes wrong
Five requirements. Clear, specific, testable. You could hand this list to another programmer (or to an AI) and they’d know exactly what to build. That’s precise thinking doing the heavy lifting, before a single line of code exists.
Step 2: Prompt AI for a Skeleton
Now you talk to an AI assistant. Not vaguely (“make me a weather app”), but specifically. What you get back is only ever as good as what you ask for. Vague question, vague answer.
A good prompt:
Write a Node.js command-line program that:
- Asks the user to enter a city name
- Fetches current weather data from a weather API using fetch()
- Displays the temperature, conditions, and humidity
- Handles errors gracefully (network issues, invalid city, missing data)
- Lets the user check multiple cities without restarting the program
Use placeholder values for the API URL and key. Include comments explaining each section.
Notice what this prompt does. It specifies the language, the interface (command line), what to use for the network request, the exact data to display, the error cases to handle, and the interaction pattern (multiple cities). It even asks for comments. That’s all communication is: the same precision you’d use giving instructions to a person who takes you very literally.
The AI generates something like this:
const readline = require("node:readline/promises");
// Configuration
const API_URL = "https://api.weatherservice.com/current";
const API_KEY = "your-api-key-here"; // Replace with your actual API key
async function getWeather(city) {
// Fetch weather data for a given city.
try {
const url = `${API_URL}?city=${encodeURIComponent(city)}&key=${API_KEY}`;
const response = await fetch(url);
if (!response.ok) {
console.log(`Error: Could not find weather data for '${city}'.`);
return null;
}
return await response.json();
} catch (error) {
console.log("Error: Could not connect to the weather service.");
return null;
}
}
function displayWeather(data) {
// Display weather information in a readable format.
const city = data.city;
const temp = data.temperature;
const conditions = data.conditions;
const humidity = data.humidity;
console.log(`\n--- Weather for ${city} ---`);
console.log(`Temperature: ${temp}°C`);
console.log(`Conditions: ${conditions}`);
console.log(`Humidity: ${humidity}%`);
console.log("-".repeat(25));
}
async function main() {
// Main loop: ask for cities until the user quits.
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
console.log("Weather Checker");
console.log("Type a city name to check the weather, or 'quit' to exit.\n");
while (true) {
const city = (await rl.question("Enter city: ")).trim();
if (city.toLowerCase() === "quit") {
console.log("Goodbye!");
break;
}
if (!city) {
console.log("Please enter a city name.");
continue;
}
const data = await getWeather(city);
if (data) {
displayWeather(data);
}
}
rl.close();
}
main();
Not bad. But don’t just run it. Read it first.
Step 3: Read and Understand the Generated Code
This is where the skills from earlier chapters earn their keep. Trace through the code like you’re reading a story.
The imports and configuration at the top: one require call (node:readline/promises, for reading terminal input), two constants for the API. Sensible. The API key is a placeholder, which is correct. You’d never put a real key in source code you share.
getWeather(city): A function that takes a city name, makes an API request, and returns the data as an object. If anything goes wrong (the network fails, or the server can’t find the city) it prints an error and returns null. That null is important. It’s how the function signals “I tried, but it didn’t work” to whoever called it.
displayWeather(data): Takes an object, pulls out the values by key, and prints them with formatting. The template literals make the output readable. The dashes create visual structure. Simple and clear.
main(): The main loop. It prints a welcome message, then enters a while (true) loop, the same pattern as the text adventure. Ask for input, check if it’s “quit,” check if it’s empty, fetch weather, display if successful. Input, process, output, repeat.
Every line here uses something you’ve already learned. Variables, functions, objects, loops, conditionals, error handling, built-ins. There’s nothing new. Only new combinations of familiar pieces.
Step 4: Run It and See What Happens
You run the program. It asks for a city. You type “London.” And… it probably fails. The API URL is a placeholder, after all.
Don’t panic. This is exactly what should happen. The point right now is to watch the structure work: the prompt appears, the input is accepted, the function is called. To make it work with a real API, you’d sign up for a weather service, get an API key, and update the URL and parameters. The documentation for the service will tell you the exact URL format and what parameters to send.
But suppose you’ve done that, and now the program runs. You type “London” and get:
--- Weather for London ---
Temperature: 12°C
Conditions: overcast
Humidity: 78%
-------------------------
It works. Mostly.
Step 5: Find and Fix Problems
You type “london” (lowercase). Does it work? You type “ London “ (with spaces). Does it work? You type nothing and press Enter. What happens?
The .trim() handles extra spaces. The empty-string check handles blank input. But what about “sdkfjhsdkfj”, a city that doesn’t exist? The API should respond with an error status, which the response.ok check catches. Good.
But wait. Try typing a city with special characters. Try a very long string. Try numbers. What breaks?
This is testing. Not formal, automated testing (though that comes later), but the instinctive poking and prodding that experienced programmers do, the way you jiggle a wobbly chair before trusting it with your weight. You’re trying to break your own creation, because if you don’t find the bugs, someone else will.
Say you discover that when the API returns a city name in a different format than what you typed (maybe you typed “new york” and it returns “New York City”), the display looks odd. Or maybe the temperature comes back as a string instead of a number. Real APIs are full of these little betrayals, and they rarely warn you first.
Fix what you find. Add a check here, a conversion there. Each fix makes the program more robust, more professional, more real.
Step 6: Add a Feature
The program works. Now make it better. Ask the AI:
Add a feature to the weather checker that shows a 3-day forecast if the API provides forecast data. Display each day’s high and low temperatures. If no forecast data is available, skip it gracefully.
The AI generates additional code. Read it carefully. Does it access the forecast data correctly? Does it handle the case where the forecast key is missing? Does the output look good?
Maybe you also want the program to remember previously checked cities and let the user re-check them:
const checkedCities = [];
// Inside the main loop, after a successful check:
if (!checkedCities.includes(city)) {
checkedCities.push(city);
}
// Add a command to list checked cities:
if (city.toLowerCase() === "history") {
if (checkedCities.length > 0) {
console.log("Previously checked:", checkedCities.join(", "));
} else {
console.log("No cities checked yet.");
}
continue;
}
This uses an array to maintain state: the same data structure from the data structures chapter, the same state concept from the interactive programs chapter. The pieces connect.
Step 7: Review and Clean Up
The code works. It has features. But is it yours?
Read through the entire program one more time. Ask yourself:
- Can I explain every line to someone else?
- Are the variable names clear? Does
datasay enough, or should it beweatherData? - Are there any comments that just repeat what the code says? Remove them.
- Are there any sections where the code does something and I’m not sure why? Investigate until you are sure.
- Could I modify this program to check something other than weather (air quality, news headlines, stock prices) without starting over?
If the answer to that last question is yes, you understand the structure. You haven’t just used AI to generate code. You’ve used AI to build something you understand.
What Just Happened
Take stock. In this chapter, you:
- Decomposed a problem into clear requirements (Chapters 0 to 2)
- Prompted an AI with a precise, structured request (Chapter 5)
- Read generated code and understood every line (Chapters 3 to 4)
- Identified data structures (objects, arrays, JSON) in the API response (Chapter 6)
- Debugged problems by testing edge cases and tracing errors (Chapter 7)
- Used a built-in (
fetch) to make HTTP requests (Chapter 8) - Extended the program with new features, maintaining its structure
That’s programming. Not any single one of those skills, but all of them together, applied to a real problem. The individual chapters were notes. This chapter was the melody.
AI as Tool vs. AI as Crutch
There’s a critical distinction here, and it’s worth being honest with yourself about which side you’re on.
If you prompted the AI, pasted the code, ran it, and moved on without reading it, the AI did the work. You were a messenger, carrying code from one place to another. You couldn’t modify it, debug it, or explain it. The moment something breaks, you’re stuck on the hard shoulder waiting for the AI to fix what the AI wrote. That’s a crutch.
If you prompted the AI, read the code, understood why each function exists, tested the edge cases yourself, and made deliberate decisions about what to change, you did the work. The AI was a tool, like a calculator or a reference book. It accelerated your work without replacing your understanding. You could rebuild this program from scratch if you had to. Maybe not as quickly, but you could do it, because you understand the why behind every what.
The difference isn’t whether you use AI. It’s whether you understand what the AI produced. The code is yours when you can explain it, modify it, and defend every choice in it. Until then, it’s borrowed.
What Comes Next
Your weather checker works. It fetches data, displays it cleanly, handles errors, even remembers what you’ve checked. Forty-odd lines of JavaScript that reach across the internet and bring back something useful. Not bad.
But imagine coming back to this code in a month, as a near-stranger who has forgotten every clever decision you made today, the way you stand in front of an open fridge with no memory of why you walked over. Will you remember what each function does? Will the variable names still make sense? If someone else needs to modify it, will they understand your choices without you hovering at their shoulder, explaining?
Code that works is necessary. Code that communicates is professional. That distinction, between software that functions and software that is readable, is what we’ll explore next.
Next: Code that works is the minimum. Code that communicates is the goal.