Kurt Vonnegut API: or, How I Stopped Worrying And Learned to Love the Process

Building the Kurt Vonnegut API

Kurt Vonnegut was, always has been, and still is, a man whose vision for a good life has made a lasting impact. His eye for the good has been a compass. My intellectual life owes him much. Here's a mantra I repeat often:

“And I urge you to please notice when you are happy, and exclaim or murmur or think at some point, 'If this isn't nice, I don't know what is.”

As I planned, built, and deployed the Kurt Vonnegut API, I kept notes of the little triumphs that had me exclaiming, "if this isn't nice, what is!" I also noted when I banged my head against the wall. I've attempted to record those moments here. You will find some technical details of this, but more so you will find reflection. Sorry. That's just the way it is. Thanks for the attention.

In the beginning...

My first GitHub commit for this project was on June 9, 2020. This was 8 months into learning to code. I chose this project purposefully. I had no interest in building any of the projects suggested by the myriad listicles of the Internet. Google "web dev project ideas." You will drown in the suggestions to "build a to-do list."

I don't know how I settled on building an API, but I did. I think what drew me was the chance to build something "on the back end." Building a server appealed to me more than building a web page.

As an amateur librarian by trade, the idea for a literary API came naturally. Combing GitHub for inspiration, I stumbled upon the anime-chan API, the Rick and Morty API, and The One API to Rule Them All. I quickly attached to the idea of building an API for Kurt Vonnegut's bibliography. It would serve metadata. I came to terms with it being a project most likely seldom used. So it goes.

Overview

The Logic

The basic logic of the API is simple:

  1. Create a Node.js server that connects to a Mongo database filled with Kurt Vonnegut data.
  2. The server listens for requests to specific endpoints:
  1. When an endpoint is pinged, a query is made to the database.
  2. The client receives a JSON response.

The DRY Principle

Implementing and building this simple logic propelled my competency with programming by a mile. "HTTP", "modular code", "abstraction" were as clear as mud to me prior to this project. The countless blog posts and YouTube tutorials -- while all well done, well rehearsed, and well executed -- simply could not compete with a hands-on approach. Having to troubleshoot incorrect HTTP payloads drastically improved my ability to understand the logic of database queries -- and drastically increased how much I cared.

A highlight of this project was implementing what I have found referred to among the online developer community as the "DRY" principle (Don't Repeat Yourself). As the name suggests, the principle encourages the absence of redundant code. Changing one section of code might affect a much larger section of code. This would hopefully reduce the amount of code you would need to write, maintain, and refactor. The creation of multiple endpoints and therefore multiple query handlers necessitated a "DRY" approach.

My proudest example was the handleQuery() function:

const handleQuery = async (req, res, field, query) => {
    const collection = await req.app.locals.collection;
    var arr = query.split(',').map(x => x.trim());

    // Change year into integer
    if (field == 'year') {
        arr = arr.map(x => parseInt(x));
    }

    // Only one fieldname, eg. title or year, not both
    if (arr.length > 1) {
        // $in matches multiple values in an array
        return await collection.find({ [field]: { $in: arr } }).toArray((err, items) => {
            return handleDbQuery(res, err, items, arr);
        })
    } else {
        // Single value
        let val = query;
        if (!isNaN(val)) {
            val = parseInt(val);
        }
        return await collection.find({ [field]: val }).toArray((err, items) => {
            return handleDbQuery(res, err, items, val);
        })
    }
}

Some context: HTTP supports adding a "query string" to a URL. We're all familiar with a URL, take https://kurtvonnegutapi.com/api for example. This is a regular 'ole URL. Now let's add a query string: https://kurtvonnegutapi.com/api?title=Slaughterhouse-Five. The ? notifies that the following characters (title=Slaughterhouse-Five) are a query in the form of a key-value pair: title is the key, and Slaughterhouse-Five is the value.

At first, I experimented with only querying by title. But as my vision for the API grew to include queries for other metadata ('year', 'form', 'genre'), it became clear that the resulting getAll() function that handled the main /api route would have serious redundancy.

For those unfamiliar with mongo syntax, a basic database query has this structure: db.collection('bibliography').find('title': 'Slaughterhouse-Five');. So instead of repeating this for each key, I created handleQuery() to accept the URL query string as a key-value pair. The resulting function for querying on the /api route was much cleaner and readable:

const getAll = async (req, res) => {
// some code...
    if (form) {
        return handleQuery(req, res, 'form', form);
    }
    if (title) {
        return handleQuery(req, res, 'title', title);
    }
    if (year) {
        return handleQuery(req, res, 'year', year);
    }
    if (genre) {
        return handleQuery(req, res, 'genre', genre);
    }
// more code...
});

For those even remotely interested, the trick was discovering that in mongo queries, variables must be enclosed with brackets []. And for those not interested, whatever, nobody asked you.

Deploying the API

I derived a lot of satisfaction from deploying the API. After combing Google, Stack Overflow, and Digital Ocean's forumns, I settled on a fairly succinct list of tech to deploy the API myself:

  1. Ubuntu 18.04 Server
  1. Nginx
  1. ufw
  1. Systemd service files
  1. screen