meta // Feb 11, 2022

The Joystick Story: How, What, and Why it Exists

I want to give a special thanks to Binil T. for prompting this post via a question on Hacker News and Alex M. for also sharing a similar interest.

I knew when I started work on Joystick I was sticking my hand into a bee's nest.

Over the last ten years or so, there's been a lot of trauma in JavaScript. Big projects going from hero to zero in just a few short years. Radical inventions—both at the language level and project level—enabling things we'd never seen before.

The pace of innovation has been rapid.

But in this flurry of creativity, something else happened. The focus—at least, as I see it—shifted from building the best tools to building the most impressive tools.

Impressive in the sense that, to an untrained eye, the trifecta of novelty, support from a big company, and the creation of the demi-rockstar developer started to mean something. Unfortunately, that meaning started to overshadow the quality of the work.

The good ol' days

When I first started cutting my teeth on the web back around 2007, JavaScript was decidedly in the background. John Resig had released jQuery the year prior and getting an accordion animation to work with one line of code made you feel like a god. JavaScript proper—stuck at ECMAScript v3 for 8 years with v5 still 2 years away—was, for most developers, not something to pay attention to.

In 2010, things started to change. By then, Node.js had launched to bring JavaScript to the back-end and on the front-end, Backbone.js was starting to become all the rage. It was around this time that I personally—a jQuery devotee—started to panic. I'll never forget the email I got from a client who was a developer himself saying "you need to learn Backbone."

Admittedly, Backbone was over my head. The concept of Models, Views, and Collections spun me like a hamster wheel. I tried desperately to figure it out, picking up courses from OGs like Geoffrey Grosenbach's PeepCode and Gregg Pollack's CodeSchool.

To no avail, I spun my tires for a few years until I decided that I could fudge my way forward with jQuery and try to figure stuff out on the side as-needed. Then, in 2012, things changed dramatically. Surfing Hacker News over lunch, I heard about a new JavaScript framework called Meteor. Around that time, I had been trying to figure out Rails but my existing programming skills rooted in JavaScript/jQuery were leaving me as empty handed as the Backbone foray.

Watching the demo video for Meteor, I had a synapse fire in my brain. I actually got it. What's more, I realized that for the first time, someone had pulled together the front-end and back-end into a cohesive package using only JavaScript that made sense. It even had real-time data (you may scoff now but this was like a caveman seeing fire for the first time) out of the box!

Captivated, I dug in. Though I didn't know it at the time, accidentally, I had started down an entirely new career path.

Learning JavaScript, for real...sort of.

By 2013, I had built up a solid enough understanding of Meteor to take a swing at building a full app. I was also starting to understand JavaScript in what I now recognize as a remedial sense—though, if you would have asked at the time I would have peacocked like I was a few notches below Brendan Eich (youthful ignorance, I suppose).

Though it was an absolute pile, late in 2013 I managed to ship an app with Meteor. It was borderline illegal, the code barely worked, but it could accept subscription payments via Stripe.

After launching, interest was—predictably—flat. Shockingly I had ~10 customers sign up before I decided to shut the app down in 2014 after having spent a year attempting a rewrite (I know, I know) and becoming increasingly concerned about the legality of the "contracts" I was minting on behalf of clients.

In preparation for the shutdown, I wanted to make sure people could export their data. I scoured the web for days trying to find an explainer on how to export a .zip file from Meteor or Node.js but there was nothing but bits and scraps. Frustrated, I decided to sit down and figure it out myself.

Out of the ashes of that came the truth. Though I had tried to convince myself that I was a software entrepreneur on the cusp of a massive ascent to financial domination, the truth was: I was a nerd. I loved writing code.

So much so that in the wake of that app, I knew I wanted to keep going. I realized I enjoyed the process of figuring out the export feature so much that I decided my next project would be a blog teaching developers how to build apps with Meteor.

Mise en place for computer geeks

In November 2014, The Meteor Chef was born. I didn't know what the hell I was doing. All I knew was that I wanted to keep learning JavaScript and working with Meteor and sharing what I was learning seemed like a good next step.

Over the next three years, I started to grok JavaScript in a way I hadn't before. Following the law of repetition, I built app after app (many destined to the hard drive graveyard), demo after demo. I wrote some 200+ tutorials and articles on software development with Meteor and JavaScript—some totaling 10,000+ words (Rain Man savant sh*t).

All of that churning led me to develop enough competency that I was able to teach others how to build their own apps 1-on-1. Nervously, late in 2015, I launched a side business under The Meteor Chef moniker called "Chef's Table." For $199, you could book a 2 hour pair programming session with me to bash on your app. Much to my surprise, it actually made money.

Around that time, too, I started to hear about a new JavaScript framework called React. First as a rumble on the Meteor forums, then, via Chef's Table clients interested to give it a go and curious if I knew anything about it. Though I didn't, I started to dig in, writing tutorials to find my way in the dark.

Slowly, I started to realize just how revolutionary React was. Up until then, the soup du jour in Meteor was their proprietary framework, Blaze, a pseudo-superset built on top of Handlebars.js which was based on templates. With React, the idea of components was a low-key mind blow (the closest you could get to that in Meteor was a dynamic template).

As I started to play with React more and more, I noticed that the JavaScript world was changing, too. ECMAScript6 was freshly released and gaining popularity, other frameworks like Vue.js (interestingly, Evan You worked at Meteor and rumor has it Vue was his vision for the next iteration of Blaze—don't quote me on that) and the Abominable Angular (sorry) were also holding their own ground.

About that time, I had spun off the Chef's Table business I was running under The Meteor Chef into a new brand, Clever Beagle, where I would teach developers how to build their own app from scratch. I'd sit down and pair program weekly with a developer to build and ship their own SaaS (in most cases) app.

Relatively quickly, I noticed that the goals of the industry—if there ever was such a thing—and the clients I was teaching was changing. Where the focus was once on building cool stuff, it had shifted to becoming purely about the tech. It wasn't about what you were building, it was about what you were building with.

As a die hard web geek who used to hole up in his bedroom eschewing a social life in favor of designing websites, this was...bothersome. Having at that point spent the last several years teaching people intentionally how to build features and full-blown apps, the contrast was stark.

Side note: I apologize to anyone who knew me personally between 2016 and 2021. I was an absolute maniac about this and probably said some horrible, horrible (but potentially funny) things.

At first, I didn't think much of it. I accepted it for what it was—fellow nerds getting excited about new toys—and passed it off as a temporary lapse.

Well, little did I know, that temporary lapse turned into a full-blown Jim Jones Kool-Aid jamboree. Over the course of a year or two, I stopped getting asked what I was building and started getting asked what stack/framework/coding paradigm I was using.

"Are you using MERN?"

"How about JAMSTACK?"

"Are you using hooks yet?!"

"When are you implementing GraphQL?"

Slowly, the cheerful, chubby spirit that guided me up to that point started to turn dark. I would get frustrated when, having committed to a set of tools, a client would softly cajole me about introducing some new tech into the project we were halfway down the road on. First, it was one. Then it was two. Eventually, I noticed that nearly every single person I was working with had gotten caught in The Faith.

Simultaneously, Meteor, the tool I had been using primarily to build my own work with as well as clients had seen its hype cycle come and go. In the wake of the unclear 2015 decision to introduce React and Angular (and eventually Vue) alongside Blaze in Meteor, the glory days of The One True Stack™ were over.

Around 2019, I started to search around for the replacement. In the tech haze of that time period, I struggled to find something that had the same panache as Meteor: an all-in-one toolkit, well-built, with a committed focus to handling the important stuff in-house (versus offloading it to a third-party package).

The closest things I found were Adonis.js and Nest.js. Though they didn't quite hit the sweet spot for me, kudos to their authors for the continued focus and longevity—I appreciate projects that take a similar bent on structure and organization. It ain't easy.

A bit lost, I started to ask myself: how would you build a framework?

The research phase

That was a massive question. In my head, the term framework had become a bit muddied along with "platform" and "library." When I thought about it in the most literal sense, I envisioned "framework" as implying structure and organization.

But structure and organization of what? Stepping back, I started to ask two broader questions: "what's bothering you?" and "what do you see confusing others?"

The commonalities were:

  • Lack of clarity around which UI framework to use and which patterns in that UI framework to use for authoring components.
  • Lack of clarity around routing and how a client-side router is different from a server-side router and how all of that plays into server-side rendering.
  • Lack of clarity around organizing data and and an explicit process for how data is passed between the client and server.

While I was able to mitigate some of this by codifying patterns into boilerplates that offered my own ideas, still, out in the wild the constant churn of tech meant that no matter how clear my opinions were about how to use a certain piece of tech (and why), I was still at the mercy of the greater consensus.

As a freshly minted tech curmudgeon, I was decidedly against the grain.

In asking these questions, what I quickly realized is that being opinionated—for all the knocks it takes—was actually a good thing. The "anything goes" attitude of web development post-2015 or so meant that nobody had a f*cking clue what the correct answer was.

Despite the cock-eyed stares I would get from clients who would listen to my town square prophet level outbursts about the "impending doom," I knew on a gut level that I was right.

Side note: this Jonathan Blow talk from 2019 is a must-watch for developers of any experience level.

So, like any good nerd, I started to build prototypes. At first, I was just building boilerplates, taking my existing knowledge and trying to understand how a framework...worked. At that point, my entire "full stack" development career primarily relied on Meteor (which obscured the screws and wires), or, my own machinations involving Doc Brown Webpack configs and NPM package collages.

I started to focus on individual parts, conjuring up the early days of my web development career and asking "how did you used to do this?"

Relatively quickly, I realized the disconnect. It was no longer clear how HTML showed up in the browser. Between all of the "server side rendering" and "hydration" talk, the lamentations I heard from newbie developers started to make more sense.

We had become so dependent on the inner workings of a particular framework that we lost sight of how that framework was doing what it did.

A "route" was no longer a route, it was a React component duct taped to the HTML5 History API. Server-side rendering—what other languages/frameworks just call "rendering"—was turned into a Rube Goldberg machine of ambiguous functions and "component trees."

Some of the more die hard nerds reading are likely mumbling "you just didn't know what you're doing" and my response is: exactly. If someone with my level of experience couldn't understand it, there wasn't a chance in hell some guy in Hyderabad trying to feed his family was going to make sense of it without developing Colitis.

All of these contraptions blurred the lines between the common goal of rendering HTML, CSS, and JavaScript in a browser and turned them into Monuments to Engineering. Even worse, the monuments kept getting torn down and replaced with new monuments! Constantly!

"Higher order components are where it's at"

"No, now we're using render props!"

"HOOKS, HOOKS, HOOKS"

"Now we're going to exume your grandmother's remains, put her back together with honey, and run some Ethernet through her corpse until she 'rehydrates.'"

F*ck, dude.

Getting past a prototype

Eventually, I decided to wipe the board. In my experimentation, what I learned was that you only needed a few parts to really build an app:

  1. A URL-based router (like Express.js) that logically mapped a URL a user would type in a browser to some HTML representing a "page."
  2. Some way to organize and generate the HTML that a URL would respond with (i.e., a component or template).
  3. API endpoints (also just routes/URLs defined in Express) that mapped to functions on the server that could be used to perform basic CRUD operations.
  4. Optionally, a build tool for compiling fancy JavaScript into dumb JavaScript.

Though this is a radical simplification, realizing this was it helped me to tap into my Zen Mind, Beginner Mind and think from first principles.

Combining that with my earlier though about opinions being good, I had a Steve Jobs grade epiphany: control the whole stack.

"The whole stack?!" I thought.

The whole stack.

But! With a caveat. Be absolutely aggressive about not reinventing the wheel. Much like that scene in Apollo 13, you have to use what you have in front of you. No new syntax. No new hacks. Just good ol' fashioned HTML, CSS, and JavaScript.

Starting with components

Admittedly, over the years I had developed a distaste for React (ironically, right as everyone was putting a ring on it). The gripe I had was that in the flurry of the "who's the biggest genius" game being played in JavaScript land, what started out as a simple function React.createComponent() had degenerated into an amorphous collection of patterns with a shelf-life of, maybe, six months to a year.

When I started work on Joystick proper early in 2021, I actually started with React as the UI framework. Having developed an understanding for the server-side rendering and hydration process, I realized that the parts to do it weren't terribly complex—they were just buried deep in the spaghetti. Mangia!

Once I got to the hot-reloading part (arguably, a standard-enough feature that I knew it had to ship in the first version) I had a small aneurysm. Once again, the path for implementing hot reloading in React was unclear. There were multiple packages, some abandoned, others promising to be shipping soon but having long been shoved to the back of the closet with the rest of the board games.

It was here, frustrated, that I opened up a blank file in VSCode and started to answer the question: "what do you want a component to look like?"

The answer? Exactly what you see in Joystick today:

  1. A single function for creating components that takes an object as its only argument, upon which methods describing the behavior of the component are written (a la React v1).
  2. A render function that returns plain HTML.
  3. Clear lifecycle methods.
  4. A way to author CSS at the component level.
  5. A way to attach events at the component level.

What I saw looked like this:

joystick({
  events: {
    'click p': () => {
      window.alert('Eureka');
    },
  },
  css: `
    p {
      font-size: 24px;
    }
  `,
  render: () => {
    return `
      <div>
        <p>This is my UI</p>
      </div>
    `;
  },
});

Plain JavaScript. Plain HTML. Plain CSS. Minimal abstraction that hints at what the underlying code is doing without forcing you to write it all by hand. In a sentence: "it renders some HTML, styles it with the CSS you provide, and attaches the event listeners you specify."

The next day, I also added in a lifecycle option with an onBeforeMount, onMount, and onBeforeUnmount callback because I haven't gotten any closer to jail than when React replaced lifecycle methods with the ambiguous useEffect()...thing.

It was so refreshing that I started to figure out the implementation two weeks later. Within about a month, I had the basic skeleton worked out but I hadn't cracked the rendering. Oblivious, I decided to research how a virtual DOM worked and much to my surprise, found an incredibly helpful talk on building a virtual DOM by Jason Yu (seriously, without Jason, I wouldn't have figured this out—on the off chance he reads this, I owe him several fancy dinners and "I'm not worthy's").

Once I worked out what he was doing in that talk and wedged it into what I was doing, much to my surprise, I had a working component library. It was rough, but it did work. Growing more familiar with his design, everything clicked. I understood how to do the re-renders. How to get the HTML in the DOM. And most importantly: how to handle the server-side rendering.

It's just rendering, dude.

Over the course of the 2-3 years I was doing research into the feasibility of this idea, I came to understand that the whole "server side rendering" fad in the JavaScript world was just...marketing. When I thought back to my earlier work with PHP or even the rudimentary stuff I cooked up in Rails, I realized I had been duped: "that's just how the web works!"

You have a server. It has some routes mapped to URLs. When one of those routes matches, the server takes an HTML file, compiles it together with some data, and returns the HTML back to the browser. Optionally, for real-time data, you can open a web socket connection and send data back-and-forth if necessary.

Once I took off the tech goggles and realized what a framework was doing (automating what I used to do back in the day—writing HTML files and embedding CSS files, and <script></script> tags), I quickly realized that I could get the rendering process to look like this without any magic:

{
  '/dashboard': (req, res) => {
    res.render('/path/to/component.js');
  },
}

Quite literally, that res.render() function takes the absolute path you give it to a Joystick component, loads the built copy (built via the build tool watching files in the root of the project and copied to the .joystick/build directory using the exact same structure as your source code), triggers the internal render() function on the component instance to get back the HTML and yeets it back to the HTTP request from the browser (wrapping it in the contents of the index.html file at the root of your project).

PWOhd3CnxUzuoQc0/mS6b1fdTSLCMhP6F.0
A Joystick build mimics your project structure 1:1 so you can understand the relationship between your source code and compiled code.

Normal, good ol' fashioned HTTP. You ask for it, you get it. One and done. As clear as you can get.

Once I had the fundamentals of this working, I knew it was just a matter of time and effort to get something shipped.

How Joystick works

A joystick project is a monolith (deep breaths). Your entire project exists in one directory.

That directory has an opinionated set of folders for organizing your project, with obvious names:

  • .joystick - where the built copy of your app is output as well as any data from databases (only in development).
  • ui contains your user interface components
  • api contains the data API/endpoints for your app
  • lib contains miscellaneous or "library" functions specific to your app
  • index.server.js is the "entry" point for your server.
  • index.client.js is the "entry" point for your client (browser).
  • index.html contains the base HTML template for all pages in your app.
  • index.css contains the global CSS for your app.

These are not all of the supported folders (other features exist like built-in i18n with language files stored in /i18n) but these are the meat and potatoes.

Inside of index.server.js, a single function is imported node as the default export from @joystick.js/node (one of three packages that make up the framework). That function is responsible for starting an Express.js server, to which the routes you pass via the routes option on the options object are assigned as HTTP GET URLs on the Express server (with an additional option to specify specific HTTP methods).

Routes are literally just Express.js routes. Though Joystick does implement some basic middleware (body parsing, cookie parsing, and user authentication via cookies) if you could do it in a vanilla Express app, you can do it in Joystick.

This is a common pattern you will notice throughout the framework. If I'm implementing an existing tool/library/whatever, I will connect the raw thing to your app and leave the APIs alone. No wrappers. No magic. No bs.

When one of your routes matches, you have the option of using the res.render() function (a hijacked version of the existing res.render()) built into Express. This function takes the path to one of your components in the /ui folder at the root of the project, converts it to HTML, hoovers up the CSS, and takes the built JS and sends them back to the requesting browser as an HTML file (i.e., automatic "server side rendering").

In that HTML file, a <script></script> tag is added to the built copy of your component on the server. When this is loaded in the browser, as part of the build process, a line of code is included for dynamically "hydrating" your component (meaning to replace the server-rendered HTML from the initial request with the dynamically rendered HTML in the browser). This is automatic and does not require any additional code to be written by you. This is the extent of the "magic" in the framework (boring, I know).

Behind the scenes, the build process is controlled via the @joystick.js/cli package which gives you access to the joystick create (to create a new project) function and the joystick start (to start an existing project) function.

When joystick start is run, the built copy of your index.server.js file (located at .joystick/build/index.server.js) is loaded, starting up your Express server (the default port is 2600wink). From there, as your routes receive requests, the rendering process above kicks in (asuming you're using the res.render() function) and requests are handled one at a time.

To be clear: only what is required for a single page is sent back to the browser. No "send all the things and then let the client take over," this is boring, missionary position web development (how God intended).

This is the "core" of the framework. Additionally, Joystick includes an accounts system (more boring stuff—it uses JWT tokens and HTTP-Only cookies to authenticate each request to the server) and a way to run a database alongside of your app in development—as of writing this is just MongoDB (PostgreSQL support is in the works as well as Redis).

When you start a database, Joystick automatically connects to it and makes its driver accessible in two places: on the req.context object of all inbound HTTP requests (on your route handlers as well as in the context object passed to Joystick's API convention, getters and setters) and globally via process.databases on the running Node.js process.

If you're curious about further details, I encourage you to read the introductory post for Joystick, the documentation, and browse the open issues I have on GitHub (I use this to track feature development as well as bug fixes).

You're also welcome to scream obscenities at me on Twitter @cheatcodetuts or send death threats to ryan.glover@cheatcode.co.

The philosophy, abridged.

The philosophy of Joystick can be summed up into one word: simplicity.

True, honest-to-goodness simplicity. I don't want to impress anyone. In fact, I often tell people "I'm retarded, but I'm not stupid." I went to a state college where I smoked entirely too much pot. I almost got a few girlfriends pregnant. Maury sh*t.

I am the walking antithesis to the Ivy League, computer scientist set (I studied Telecommunications in college). I have no tact—don't care to find any—and my respect for authority and the status quo is trending negative.

But, my redeeming quality: I'm a nerd and I care deeply about making web development accessible to others. And when I mean others, I don't just mean other people with well-endowed salaries and preferred customer status at West Elm.

I mean the sweet guy I worked with in Ethiopia who routinely jokes about a small scale war going on a few miles from where his family lives. I mean the Australian guy who works for a small business building their software who has graciously allowed me to shepherd his work for the past few years. I mean the hospital administrator I've been teaching who's psyched about shipping his first piece of software next month that he invested in on the side.

Normal people.

I want the team that has to cobble together internal software for a bank to actually get home from work and not want to end it all. I want people to enjoy web development again. I want the young excited 23 year old dork with an attitude problem but a deep fascination with websites to be empowered to build his idea, no matter how silly, without having to trade his 20s to figure it out (theatrics aside, I enjoyed every minute).

Web development doesn't need to be confusing. You should be able to fully understand what's happening and when with minimal stress. At the framework level, you should have all of the basics (components, routing, data fetching, RPC, file uploads, etc.) out of the box, only reaching to third-party packages for the truly esoteric or domain-specific work.

Joystick's long-term vision is to be the Honda Civic of JavaScript frameworks. Reliable, easy to pick up, and long-lasting. What you build with it today will make sense and work in ten years—this isn't just another empty promise; that was the intent the framework started with. You should be able to build a business on Joystick and trust that it will continue to work with minimal maintenance and stress—whether you're shipping a new startup or building internal tools for an established business, Joystick is the tool you can count on to get the job done.

The truth and nothing but the truth

Admittedly, as I was building Joystick, I felt like a moron. I had bitten off a gigantic piece of steak, put my reputation on the line (what minuscule notoriety I had would have faded into obscurity had this not worked), and would have been the "oh yeah, whatever happened to that idea" punching bag. Well guess what, motherf*ckers: Daddy delivered.

And I want to be clear. Though I do have a reputation for having an acerbic wit and antagonistic nature (I'm fully aware that I'm pissing you off when I do it—generally for fun, however psychotic that seems), if and when I "call out" other developers, I'm doing it to get a rise out of people; not to be a genuine dick.

If I do hurt your feelings, I encourage you to get in touch so we can hug it out, get drunk together, and say all the things we wouldn't normally say in polite company (ask around, when I get going, it's a solid hang).

In terms of intent: I hate venture capitalists. Zip up sweaters annoy me. And that feigned "I'm interested but I'm actually not I just want to milk you like a blue ribbon cow" attitude works about as much charm on me as putting lit cigarettes out on my cheek over an unpaid gambling debt.

Does that mean I'll never sell what I'm building? No. If I do, though, I already know exactly who I'd proposition a sale to (or at least work with as a broker). And yes, they have a reputation for actually respecting what you create so even if I f*ck off to South America to start a low-key polygamy operation, you can trust that what I build will persist long-term.

Please, please, please do not send me requests to "hop on a quick chat" or "run the numbers." I will ruin your psyche and worldview so quickly you'll end up in an institution.

That's the story for now.

To progress.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode