The WWW as a Rube Goldberg machine

The Web was built around the concept of requests and responses - a constant back and forth between a server on the Internet and a browser on your computer. As it grew and expanded the need arose to get exchange going not just between a person and a machine but among many machines.

A user initiates a single action - a click of a button, a text entry, a page scroll - and the browser triggers a chain of events, most of them hidden behind the veil of the Cloud, to pass around some data from one server to another, like a ball in a pinball machine. The data can be used to request other data, to trigger different responses from servers, to capture more data. It can be formatted, rearranged, displayed in different contexts, - juxtaposed with other data.

In the spirit of Rube Goldberg's fascination with the mechanism over the end result - how can we build similar systems in the digital realm? What kind of data could be passed around? With services could be chained together to produce some unexpected outcomes? What would the triggers look like - and the end results? None of those need to be practical, efficient or even reasonable - the focus is on the celebration of the chains of events and the mechanisms that enable them.

Example 1: Daisy-chaning fetch requests to APIs

Jump to example2: openAI API

We will create a rudimentary example of how you can request some data from an API, append it to you Web page, then once this is done - request more data from another API and continue the process meant to invoke a Rube Goldberg machine.

The result should resemble this example

  1. We'll start by creating a basic HTML page. The example is simple enough that we don't need a complex file structure and everything will be done from one file. You can use a basic boilerplate HTML for the start: https://htmlboilerplates.com/

    We need to trigger our chain of events, so add a button to serve as a trigger:

    <body> 
        <button role="button" id="initTrigger">START!</button> 
    </body>

    We'll need to prepare some sections in the document to help us structure the content we'll be adding once we get it from the APIs, so let's add 2 sections. The rest of the work we'll do with JavaScript, so we'll add a script element into our document and start writing the rest of the content inside it.

    <body> 
        <button role="button" id="initTrigger">START!</button> 
        <section id="cocktailSection"></section> 
        <section id="NASAimage"></section>
        <script> </script>
    </body>
  2. You might have guessed from the section IDs that the first API we'll use is a CocktailDB API - a repository of recepies for alcoholic and non-alcoholic drinks. The sesond one will be one of APIs hosted by NASA called Astronomy Picture of The Day, or APOD

    They have their diffirences, but the main similarity is that they both are easy to acces and return their results in JSON. JSON is a data format - in fact, it's the most common data format on the Web (at least in 2023.) You can see what it looks like if you copy one of the example URLs from Cocktail DB - let's say this one: www.thecocktaildb.com/api/json/v1/1/random.php and open it in a new tab in your browser.

    If you are using Firefox you will probably see something like the image below.

    Example of formatted JSON data

    In Chrome, without a built-in JSON formatting tool it will probably look different (see below). You can get a JSON prettifier or just switch to Firefox!

    Example of unformatted JSON data
  3. Now that we know what kind of data we can expect in return to our requests, and how to get them manually we can write a script to fetch those results for us. We'll move into our <script> and write some JavaScript to do that. First we'll need to create a reference to our button, so that we can track when it's clicked.

    const triggerButton = document.getElementById('initTrigger')

    Then we'll need to create an event listenter on that button, so that when the event we're looking for (a click on the button) happens - we'll trigger a function in response. The function we'll be triggering doesn't exist yet, but we'll create it afterwards add call it "fetchCocktailData"

    triggerButton.addEventListener("click", fetchCocktailData)

    Now we can make the function. We'll be using JavaScript Fetch API to make our requests to the servers. The main reason why we need to use it is because we don't know how much time the server we'll take to respond to us with the data we're looking for. We'll need to hold the execution of dependent functionality until we get the data and Fetch API will help us.

    We'll fetch the data from a URL, then extract just the JSON payload, sans the headers and other metadata, and then we'll log the result into the browser concole to verify. The URL we'll is the same random cocktail URL and we'll store it in a constant for convenience. Here is what the contents of our <script> should look like now

    const triggerButton = document.getElementById('initTrigger')
    
    const randomCocktailURL = "https://www.thecocktaildb.com/api/json/v1/1/random.php"
    
    const fetchCocktailData = () => {
        fetch(randomCocktailURL)
            .then((response) =>  response.json() )
            .then((payload) => console.log(payload))
    }
    
    triggerButton.addEventListener("click", fetchCocktailData)

    If you launch your HTML file in a browser, open the console and click the "Start" button you should see a JSON payload very similar to what you saw whe you launched the URL in a browser tab.

  4. Now that we have access to the data through our script we can add a function to parse the data and add it to our page instead of just logging it into the console. The data that comes in JSON format would consist of objects where data is stored in key-value pairs and arrays where data is stored under indices. To access data in an object we'll use object.key format and for the array we'll use array[index] format. To find which should be used we'll need to look closely at the structure of the data received - that's why logging it into the console is useful.

    const appendCocktailData = (data) => {
        console.log(data)
    
        const firstDrink = data.drinks[0]
        const drinkName = firstDrink.strDrink
        const drinkImg = firstDrink.strDrinkThumb
    }

    To append the data we'll need to first create an element in the document, then add our data to its properties as required, and then append it to the apropriate section in the document. For that we'll also need to add the reference to that section - we can place it at the top of our script next to the reference to the button.

    const triggerButton = document.getElementById('initTrigger')
    const cocktailSection = document.getElementById('cocktailSection')

    The function 'appendCocktailData' should now look like this:

    const appendCocktailData = (data) => {
        console.log(data)
    
        const firstDrink = data.drinks[0]
        const drinkName = firstDrink.strDrink
        const drinkImg = firstDrink.strDrinkThumb
    
        const nameHeader = document.createElement('h3')
        nameHeader.innerText = "We had " + drinkName
        cocktailSection.appendChild(nameHeader)
    
        const image = document.createElement('img')
        image.src = drinkImg
        image.alt = "image of " + drinkName
        cocktailSection.appendChild(image)
    }

    We can now call this function once our data is fetched and parsed. The script should now look like this:

    const triggerButton = document.getElementById('initTrigger')
    const cocktailSection = document.getElementById('cocktailSection')
    
    const fetchCocktailData = () => {
        fetch(randomCocktailURL)
            .then((response) =>  response.json() )
            .then((payload) => appendCocktailData(payload))
    
    }
    
    const appendCocktailData = (data) => {
        console.log(data)
    
        const firstDrink = data.drinks[0]
        const drinkName = firstDrink.strDrink
        const drinkImg = firstDrink.strDrinkThumb
    
        const nameHeader = document.createElement('h3')
        nameHeader.innerText = "We had " + drinkName
        cocktailSection.appendChild(nameHeader)
    
        const image = document.createElement('img')
        image.src = drinkImg
        image.alt = "image of " + drinkName
        cocktailSection.appendChild(image)
    }
    
    triggerButton.addEventListener("click", fetchCocktailData)
  5. Time to add the next link in our Rube Goldberg machine - a call to another API, this time NASA APOD. While CocktailDB API does not require any user accounts or other forms authentication, NASA encourages their API users to obtain their own authentication key. Doing so will expand the functinoality and does not cost anything, so it's not a bad idea. Go to https://api.nasa.gov/#signUp and use your email to create your own key.

    Then we need to figure our which URL to send our requests to to get the data we're looking for. If you open the details of APOD API you will see that all requests should go to the same URL - https://api.nasa.gov/planetary/apod but there is a set of query parameters that can be apended to the URL to modify your request. Those parameters include api_key, date, count and others. To add those parameters to the query we need to first add a ? after the URL and then separate each query parameter by an &. For example:

    https://api.nasa.gov/planetary/apod?api_key=YOUR_OWN_API_KEY&date=2003-10-29

    Some of these parameters might change so it helps to separate them in our code. We'll re-assemble them later when it's time to use them, but for now let's create a set of 3 constants: for the URL and for each of the query parameters, and pust if next to our cocktailDB URL constant:

    const nasaAPODurl = "https://api.nasa.gov/planetary/apod"
    const nasaAPIkey = "api_key=YOUR_OWN_API_KEY"
    const nasaDate = "date=2003-10-29"

    The date of October 29, 2003 is completely random, feel free to put any other one you like.

    Now we can create a fetch function for the NASA API and we'll trigger it right after we're done appending the content from the CocktailDB. At the end of appendCocktailData add the call to fetchNASAData:

        // other previous code here                
        const image = document.createElement('img')
        image.src = drinkImg
        image.alt = "image of " + drinkName
        cocktailSection.appendChild(image)
        // ADD THIS LINE:
        fetchNASAData()
    }

    You could also create a version where you extract a piece of data from one API response and use it as a parameter for the next call. For example, we could take the date on on which the cocktail recepie was last modified and use it instead of our random date in the call to the NASA API. For that we'll need to make sure that the date also fits the format that the NASA API expects - you will typicaly find what it is in the API documentation. Comparing the data we can see that the cocktail `dateModified` contains hours, minutes and seconds, while the NASA API expects just a year, month and day separated by dashes. one easy way to work around that is to convert `dateModified` into a JavaScript Date object and then extract the year, month and day and re-assemble them via one of the string concatention methods. Notice that `fetchNASAData` function now takes an agrument and we're passing the formatted date to it.

        cocktailSection.appendChild(image)
    
        const a = new Date(firstDrink.dateModified)
        const dateForNASAAPI = a.getUTCFullYear().toString()+"-"+(a.getUTCMonth()+1).toString()+"-"+a.getUTCDay()
    
        fetchNASAData(dateForNASAAPI)
    }

    Now lets create the fetchNASAData function. I put it right below the appendCocktailData function in the example but you are welcome to organize your code as you prefer. The functionality is veryb similar to how we fetched the data from CocktailDB, the only thing we need to do first is to assemble the URL and the query parameters to get the right response.

    const fetchNASAData = () => {
        const fetchURL = nasaAPODurl+"?"+nasaAPIkey+"&"+nasaDate
        fetch(fetchURL)
            .then((payload) =>  payload.json() )
            .then((json) => appendNASAData(json))
    }

    Or if you wanted to use a date supplied by another API response, you could pass the date as an argument to the function and then use that instead of pre-set constant `nasaDate`.

    const fetchNASAData = (picDate) => {
        const fetchURL = nasaAPODurl+"?"+nasaAPIkey+"&"+"date="+picDate
        fetch(fetchURL)
            .then((payload) =>  payload.json() )
            .then((json) => appendNASAData(json))
    }

    At the end of the fetching sequence, in the last "then" you will notice a call to a new function - appendNASAData. We have not made it yet, but from the previous example you might have guessed what it does. Let write it out.

  6. Similar to CocktailDB we need to parse the data to extract just the components we need. Unfortunately, very API structures its responses differently, so we'll need to look at the data structure to figure out how to get to the specific values - remember that in an object we'll use object.key format and for the array we'll use array[index] format. Remember to console.log the data to see how it's structured.

    After that we can create our document elements and put the data into their apropriate properties. Then we can append them to the section of our docuemnt allocated for NASA data - don't forget to include the reference for it at the top, ner the references to the button and the cocktail section:

    const triggerButton = document.getElementById('initTrigger')
    const cocktailSection = document.getElementById('cocktailSection')
    //NEW ONE:
    const NASAsection = document.getElementById('NASAimage')

    The whole appendNASAData function would look like this:

            
    const appendNASAData = (data) => {
        console.log(data)
    
        const picTitle = data.title
        const imgURL = data.url
    
        const picHeader = document.createElement('h3')
        picHeader.innerText = picTitle
        NASAsection.appendChild(picHeader)
    
        const nasaImg = document.createElement('img')
        nasaImg.src = imgURL
        nasaImg.alt = "the image of " + picTitle
        NASAsection.appendChild(nasaImg)
    }
  7. This concludes the first example. You can continue adding for fecth*** and append*** function to this chain of events, and add additional content to construct a story around the data you are receiving. You can also use some data comonents of a previous response - like title or description - to server as query paremeters in the following fetch data requests.

    You can see the entire example code here and try in in action here.

Example 2: simple use of OpenAI API

This example show a very simple use-case for OpenAI API. There are many models included in the public API offerings, and many ways to interact with them - which is great, but also brings a new level of complexity. To manage this complexity OpenAI API uses several additional tools that we'll to intall and utilize to make use of their system.

The result should resemble this example

  1. The descriptions and examples in OpenAI API documentation come in variants: Python and Node.JS. We'll be using NodeJS because it's written in JavaScrips and can be easily made to work on the Web. We need to start by ensuring that you have it installed in your system. I will be using MacOS in examples and illustrations, but it works the same on Windows (just looks a little differently.)

    Start by launching your Terminal (PowerShell on Windows.) Once it's running type in the command:

    node --version

    If you see a version number and it's higher than 14.0.0 - you'll be fine. If you get a response that amounts to "I don't know what you want from me!" you'll need to install NodeJS from their website: nodejs.org. Grab the version recommended for the most users and proceed with the installation. Once it's finished it might not be a bad idea to restart your machine - just to make sure all the paths are updated. After that try launching Terminal (PowerShell on Windows) and running the command again.

    node --version

    You should see the version number now. Also run this command:

    npm --version

    You should see a number higher than 6.0.0 If you don't - consult this guide for details "Downloading and installing Node.js and npm"

    If you think of a browser as a car NodeJS is the engine from that car, that has been extracted and packaged and can be installed in your garage (on your computer) to provide power to your tools. NPM is Node Package Manager, and it will help us manage and install new components for that engine. You ternimal (or PowerShell on Windows) should look something like this now:

    Terminal view with Node and NPM version commands
  2. We'll need to create a simple Node-based Web application and run it in a local server to make sure that the requests to OpenAI API come from an HTTP host and not just a file in your filesystem. There are many ways of doing this - one of the simpler ones is to create an app with Vite, a scaffolding engine.

    Before we create our app let's figure out where will we be building it. To find your current working directory run

    pwd

    command in your Terminal (or PowerShell on Windows.) This will print out the path. From there you can navigate to the directory where you'd like to build your app. For example, I prefer to keep my coding projects in the "Repositories" directory inside my user folder so to move there from my current personal user folder I will run a cd (change directory) command like this:

    cd Repositories

    Once there I will run a simple command to create a Vite-based web-app:

    npm create vite@latest

    and follow the prompts. I will name my project "openAIapiProject" and we'll be using vanilla JavaScript for this project. So my terminal will look like this:

    Terminal view showing Vite install sequence
  3. Once you've created your web-app Vite will prompt you with 3 next steps: change to the newly created app directory (its name is the same as the name you gave your Vite project), install the packages necessary to run your app and then run it in a developer mode. After completing the first 2 steps your Terminal (or PowerShell) would look similar to this:

    Terminal view showing Vite instructions 1 and 2

    Now you should be ready to run your app. Run the third command (npm run dev) and your NodeJS server should start. If you press "h" for help you should be able to see this:

    Terminal view showing running Vite server

    You can press 'o' to open the default page in your browser. Vite server will track any chages you make to your web-app and will restart this browser page the moment you save your changes to your file, which is neat. So check back here as you progress with your work.

  4. Open the directory that Vite has created for your web-app in your code editor and you'll see that there are a lot of things there that we won't need. You can start deleting them

    File structure view showing what to delete in Vite: public folder, counter.js and javascript.svg

    You can also remove evrything from "style.css" and leave only one line in main.js, the first one:

    import './style.css'

    Save all your changes and check back with your browser - you should see a blank page instead of the default page you saw before. You can check on the resulting file structure here. The last thing we need to do in your Terminal is to instal openaiapi library for NodeJS. In your Terminal press 'q' to quit your Vite server and run

    npm install openai

    Once the installation has finished you can run

    npm run dev

    again to restart the Vite server.

  5. OpenAI API requires and API key, and has no demo or account-less options. You'll need to sign up for a free account with them and then head over to platform.openai.com/api-keys to get your key. The good news is that you'll receive $18 in account credit with you can spend on your AI-related experiments. Each image generation will cost you 2-4 cents, so you should be ok for a bit.

    Open your main.js file in your web-app and add a new line after the CSS import

    import './style.css'
    import OpenAI from 'openai'

    Then we'll instantiate the OpenAI library with our API key. We have to add "dangerouslyAllowBrowser" set to "true" because we are storing our API key directly in the code and OpenAI developers consider it dangerous. For a different way to set up your API key you can refer to this docuemnt https://platform.openai.com/docs/quickstart/step-2-setup-your-api-key:

    const openai = new OpenAI({ 
        apiKey: "YOUR_API_KEY_HERE'",
        dangerouslyAllowBrowser: true
    })

    Now we can create a request for OpenAI API to generate an image for us using createImage method. There are other methods in the API that you can find here. ChatGPT is availableas well and DALL-E 2 and 3. You need to pass the prompt, the number of images we want generate and the image size - either 1024x1024, 1024x1792 or 1792x1024 pixels. Larger images cost more. The await command with work similarly to the FetchAPI functionality - it will halt the execution of the subsequent code until the request has been populated with the server's response.

    const response = await openai.images.generate({
        model: "dall-e-3",
        prompt: "A cat wearing armor riding a TRex into battle",
        n: 1,
        size: "1024x1024",
    })

    After that we can console.log the response to review the data structure and use our previous knowledge of how to get to specific values (To access data in an object we'll use object.key format and for the array we'll use array[index] format.) We can then create an element in the document and append it to the body of the page to display to a viewer.

    console.log(response)
    
    const img = document.createElement('img')
    img.src = response.data.data[0].url
    img.alt = "A cat wearing armor riding a TRex into battle"
    document.body.appendChild(img)
  6. You can view the entire code here. If you want to run it you'll need to clone or download that repo and then navigate to the OpenAIApiExample directory in your Terminal (or PowerShell on Windows.) Then you'll need to run

    npm install

    and then

    npm run dev

    to install all the components and run the Vite server locally.