meta // Oct 27, 2021

Introducing Joystick: The Full-Stack JavaScript Framework

Today, I'm beyond thrilled to introduce you to Joystick.

Joystick is a full-stack JavaScript framework. Right now, that consists of a front-end UI library @joystick.js/ui, a back-end library @joystick.js/node, and a command-line interface @joystick.js/cli. The ui and node packages can be used independently, but are designed and intended to be used together.

Joystick is free and MIT-licensed. No, you won't have to sell me your soul to access it or worry about financing my future ambitions in eugenics or authoritarianism. Consider this my thank you gift to all of the developers who came before me, all of the developers just getting started, and everyone in between.

If you want to dig in, head over to the repo on Github and check out the docs.

The front-end

@joystick.js/ui brings a component-based approach to writing HTML, CSS, and JavaScript in a more organized, friendly way. It does not introduce any new languages or syntax. Components built with @joystick.js/ui look like this:

Example Joystick Component

import ui from "@joystick.js/ui";
import Quote from "../../components/quote";

const Index = ui.component({
  methods: {
    handleLogHello: () => {
      console.log("Hello!");
    },
  },
  events: {
    "click .say-hello": (component) => {
      component.methods.handleLogHello();
    },
  },
  css: `
    div p {
      font-size: 18px;
      background: #eee;
      padding: 20px;
    }
  `,
  render: ({ component, i18n }) => {
    return `
      <div>
        <p>${i18n("quote")}</p>
        ${component(Quote, {
          quote: "Light up the darkness.",
          attribution: "Bob Marley",
        })}
      </div>
    `;
  },
});

export default Index;

The render() function for components piggybacks on JavaScript's template literal convention. You return a string of HTML from it, using JavaScript interpolation to embed one of a few render functions: component(), each(), i18n(), and when().

component()allows you to render another component (enabling composition of components), each() helps you loop over and render data in arrays, i18n() helps you render internationalization strings, and when() helps you conditionally render HTML based on some variable.

In addition to the render() method, components have a few other options:

  • css which can be set to a string of CSS (or a function which takes in the component instance and returns a string of CSS) that's dynamically scoped to your component and injected into the page.
  • events which are JavaScript DOM events dynamically scoped to your component.
  • methods which are miscellaneous, domain-specific functions for your component (and can be called by accessing the component instance passed to all other options).
  • lifecycle which is a fixed set of methods—onBeforeMount, onMount, and onBeforeUnmount—that fire in relation to the lifecycle of the component.
  • state an object (or function returning an object) that represents the default state of the component and can be modified via the .setState() method on the component instance.

@joystick.js/ui also exports a get() and set() function for performing HTTP GET requests and HTTP POST requests to your API (more on this below), using input validation and Joystick's SelectiveFetch to customize the output returned from the API. They look like this in use:

get() example

get('posts', {
  input: {
    category: 'tutorials',
  },
  output: ['title', 'author.name', 'publishedAt']
}).then((data) => {
  // Use data returned here...
});

set() example

set('createPost', {
  input: {
    title: 'A new blog post',
    body: 'The body of the post.',
  },
  output: ['_id']
}).then((data) => {
  // Use data returned here...
});

For user accounts, an accounts object is also exported from @joystick.js/ui with five methods on it:

  • signup() which is used to create new users.
  • login() which is used to login existing users.
  • logout() which is used to logout an existing user.
  • recoverPassword() which is used to initiate a password reset.
  • resetPassword() which is used to reset a password.

That's the front-end. The goal is to keep this API fixed, only adding things (if absolutely necessary) as time passes. Components you write in Joystick today will work with Joystick in 10 years. My foot is down on this.

The back-end

On the back-end, @joystick.js/node is a thin layer over Express.js along with some other features. The centerpiece being the .app()method accessible via the object exported from the package (you will most commonly see this in boilerplate and example code as node.app()).

The node.app() function is responsible for:

  • Registering your routes which are normal Express.js routes with a thin layer of syntactic sugar over them to keep things organized. Routes are assigned a special res.render() function to help you render Joystick components.
  • Registering your api built using @joystick.js/node's getter and setter functions.
  • Registering any custom middleware to run before each route.
  • Registering any listeners for standard Node.js events like uncaughtException or unhandledRejection.

More functionality will be added to node.app() as Joystick approaches a 1.0.0.

For routes, res.render() is the only magic trick in the framework (the rest is just plumbing with pants pulled up).

This function takes in the path to a Joystick component in your ui/pages directory and does a few things:

  1. Server-side renders the page as static HTML and CSS.
  2. Automatically embeds the script representing the compiled JavaScript version of the component to be loaded in the browser.
  3. Utilizes the automatically injected mount script in @joystick.js/cli to automatically "hydrate" the component on the client (swap the SSR rendered HTML with the dynamic JavaScript version).

For you, this means super-fast mostly hands-free rendering of pages/components. The only thing sent to the browser is what's needed to render that page.

For the API portion of node.app(), Joystick introduces the concept of getters and setters. They look like this:

Defining a schema in Joystick

{
  getters: {
    posts: {
      input: {
        category: {
          type: "string",
        },
      },
      get: (input, context) => {
        const query = {};
        
        if (input.category) {
          query.category = input.category;
        }
        
        return context.mongodb.collection('posts').findOne(query).toArray();
      },
    },
  },
  setters: {
    createPost: {
      input: {
        title: {
          type: "string",
          required: true,
        },
        body: {
          type: "string",
        },
      },
      set: async (input, context) => {
        const postId = joystick.id();
        await context.mongodb.collection('posts').insertOne(input);
        return {
          _id: postId,
        };
      },
    },
  },
}

All of your getters and setters are organized into one object (referred to as your API's schema), with each getter or setter nested under the object of its respective type.

Getters and setters are nearly identical save for their intent. They allow you to define validation for any input passed to them and a function get() or set() to respond to the request. Input validation uses a library that's built-in to Joystick.

You'll also notice that the get() and set() functions receive a context argument alongside the input. This gives you access to the HTTP req and res objects from Express.js (getters and setters are implemented using Express.js routes just like your other routes) as well as any databases you've loaded via the CLI (more on this below) and, if applicable, the current user.

The final piece of string tying all of this together is @joystick.js/cli, a command-line interface (CLI) that helps you to create new Joystick projects and start a development server for existing ones. It also helps you to start databases in development and communicate with @joystick.js/node to load the appropriate driver so your database is automatically available in your app.

Project creation is simple. After installing @joystick.js/cli via NPM (npm i -g @joystick.js/cli), just run joystick create <project> where <project> is the name of the directory your project will be created (e.g., joystick create banana-stand).

To start your development server, cd into your Joystick project's folder and run joystick start to start your app at http://localhost:2600 (if you're not an Atari fan, you can run your app on any available port with joystick start --port <port>).

The databases @joystick.js/cli starts up for your app in development are determined by the config.databases array in your app's settings.development.json file (automatically created for you via joystick create).

Once started, the native Node.js driver for each database started will be accessible in one of three ways (each equal and purely existing for convenience):

  • On your routes defined via node.app() as part of the req.context object. Each database is available by its name here (e.g., req.context.mongodb or req.context.postgresql).
  • On the context object passed to the get and set functions on your getters and setters in the API.
  • Globally via the Node.js process object with each database being defined by name (e.g., process.mongodb or process.postgresql).

Joystick makes the conncetion to your database (which can be configured as part of the config.databases array in your settings) but does not modify the native behavior of the database or its driver. Once connected, you're at the mercy of the driver and its documentation (and my benevolence).

As part of this setup, Joystick can also selectively map your user accounts to the databsae of your choice. Just set users to true in your config.databases array and Joystick will handle the plumbing.

That's the framework. There are more, smaller features explained in the documentation.

Joystick is proprietary

I've had enough bullets ricochet off my helmet in the trenches to know that an Apple-style "walled garden" approach to Joystick's development is best.

As much as possible—and makes sense—Joystick will be focused on giving you what you need out of the box with usage of third-party libraries being reserved for the truly esoteric, uncommon stuff.

The goal of this is two-fold: to reduce the amount of messy, fragile code in your app and to offer a stable, long-term focused framework that doesn't leave you out in the cold on functionality that should be built-in.

This concept will be foreign to and rejected by many people, but in time I hope to slam my scepter down at the base of my throne and hear the electorate cheer in approval.

Joystick is opinionated

Joystick is highly opinionated. The battlefield trauma I've experienced as a mentor to developers has led me to develop a "get off my lawn" attitude towards how things should be done. What I've learned over the years is that, without a clear, definitive process, Murphy's law kicks in and any code that can be written, will be written.

Folder and file structure are specific and forced. The feature set will be 100% driven by my own mental illness and input from developers actively using the framework (not will-o-the-wisp computer science graduates who've failed to wipe the ketchup off their shirt having a bad morning). For now, pull requests will be turned away unless they're carrying a supreme pizza and delivered by an attractive brunette woman.

Joystick's API will not be subject to random change. At worst, I'll add features, but no "Hey, guess what?! You have to refactor your entire codebase to keep using the framework! Awesome!"

The design of Joystick was and is highly intentional and highly labored over. I sincerely don't care if you think you're better than or know more than me. I will handle people like this with far less tact than Steve Jobs.

Joystick will be in beta for now

Why a beta? Well, up until now, barely anybody knew I was working on this (and I'm fairly certain the ones that did thought I was full of 💩), which was intentional.

As a result, I only know about the problems I know about. While things do work in general, I'm careful to give them my final stamp of approval as I'm certain there are plenty of bugs lurking in the bushes. Today's release is less about perfection and more about coming out of obscurity. I want to spend the next few months writing tests, clarifying APIs and documentation, and thinking about any loose ends in terms of feature set before I say "this is a 1.0."

Expect a dash of chaos while I stitch the bird up before putting it in the oven. If you're looking to build a mission critical app: wait until the 1.0 drops (unless you have a gambling problem, then go for it).

That said, you're encouraged to play. Find stuff that doesn't work. Find stuff that doesn't make sense. But for the love of God, don't leave me passive aggressive comments on Github: I won't be pleasant and will go out of my way to publicly embarass you. If you're kind and honest: I will climb over mountains for you.

And here...we...go

Admittedly, this is picking a fight. But it's a fight I feel is necessary and meaningful. To be blunt: web development with JavaScript has gone off the rails. It's left its family, bought a bunch of Tommy Bahama shirts, moved to South America and impregnated a young Peruvian girl.

Having a midlife crisis is fine—acceptable, even, if done tastefully—but it's not conducive to building great software. Developers old and new need stable software that's easy to understand and keeps them productive—not a never-ending rug pull with passive shouts of condescension.

This will absolutely piss some people off and I welcome their discontent. If JavaScript is going to move in a positive, sane direction long-term (and I'm going to avoid jail time), it's necessary.

Ante up.

Start building with Joystick

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode