Just Released: Joystick RC1

Take control of your app

Joystick is a full-stack JavaScript framework for building stable, easy-to-maintain apps and websites.

v1.0.0-rc.1 — Supports MacOS, Windows, and Linux. SAUCR licensed.

~/projects/joystick/example-app

  • api/books/getters.js

  • api/books/setters.js

  • api/index.js

  • index.server.js

  • settings.development.json

  • ui/components/books_list/index.js

  • ui/layouts/app/index.js

  • ui/pages/books/index.js

import joystick from '@joystick.js/node';
import api from './api/index.js';

joystick.app({
  api,
  routes: {
    '/books': (req = {}, res = {}) => {
      res.render('ui/pages/books/index.js', {
        layout: 'ui/layouts/app/index.js',
      });
    },
    '/books/:book_id': (req = {}, res = {}) => {
      res.render('ui/pages/book/index.js', {
        layout: 'ui/layouts/app/index.js',
      });
    }
  },
});

Terminal

  • Starting MongoDB...
  • Starting app...
  • App running at: http://localhost:2600
Iterate Faster

A lighting-fast development environment.

Joystick's CLI automatically starts up your app's databases, HTTP server, and enables HMR for near-instant updates in development.

Start your app + databases in seconds

Get to work fast and stay productive with clear, easy-to-understand logging designed for rapid iteration.

joystick start

Starting MongoDB...

Starting app...

App running at: http://localhost:2600

Near-instant updates with HMR

Rapid development is easy with near-instant builds via esbuild and HMR updates in the browser.

Run tests to build confidence

Start up a mirror of your app and run tests once, or in watch mode to test as you develop.

joystick test --watch

Deploy your app globally

Ready to go live? Tailor your infrastructure in your app's settings file and run joystick push from your project folder.

joystick push

Build with Components

Easy-to-grok components; no CS degree required.

Use plain HTML, CSS, and JavaScript to build your components, fast, with zero hacks or confusion.

Render standards-based HTML

If it's in the spec, it works with Joystick. Go from learning HTML basics to building components without the usual rug pulls.

const VideoPlayer undefined joystick.component({
  render: ({ props, each }) undefined> {
    return undefined
      <div classundefined"video-player">
        <video>
          ${each(props.sources, (source) undefined> {
            return undefined<source srcundefined"${source.src}" typeundefined"${source.type}" />undefined
          })}
        </video>
      </div>
    undefined;
  },
});

Author scoped CSS on the component

Write CSS as a simple string, or, use Joystick's breakpoint syntax to whip up a responsive UI, quick. Styles are scoped automatically.

  • String CSS
  • Breakpoint CSS
const VideoPlayer undefined joystick.component({
  css: undefined
    .video-player {
      border: 1px solid #000;
      padding: 20px;
      border-radius: 5px;
    }
  undefined,
  render: ({ props, each }) undefined> {
    return undefined
      <div classundefined"video-player">
        ...
      </div>
    undefined;
  },
});
const VideoPlayer undefined joystick.component({
  css: {
    min: {
      width: {
        0: undefined
          .video-player {
            border: 1px solid #000;
            padding: 20px;
            border-radius: 5px;
          }
        undefined,
        768: undefined
          .video-player {
            padding: 30px;
          }
        undefined
      }
    }
  },
  render: ({ props, each }) undefined> {
    return undefined
      <div classundefined"video-player">
        ...
      </div>
    undefined;
  },
});

Fetch data on the component

Tell Joystick what data you need for your component and it will fetch it during SSR. Fetch data on the client, too—all using simple APIs.

  • SSR Fetch
  • Client Fetch
const UserAdmin undefined joystick.component({
  data: async (api undefined {}) undefined> {
    return {
      users: await api.get('admin_users'),
    };
  },
  render: ({ data, each }) undefined> {
    return undefined
      <div classundefined"user-admin">
        <ul>
          ${each(data?.users, (user undefined {}) undefined> {
            return undefined<li>
              <a hrefundefined"/admin/users/${user?._id}">
                ${user?.email_address}
              </a>
            </li>undefined;
          })}
        </ul>
      </div>
    undefined;
  },
});
import { get } from '@joystick.js/ui';

const UserAdmin undefined joystick.component({
  state: {
    users: [],
  },
  lifecycle: {
    on_mount: (instance undefined {}) undefined> {
      get('admin_users').then((users) undefined> {
        instance.set_state({ users });
      });
    },
  },
  render: ({ state, each }) undefined> {
    return undefined
      <div classundefined"user-admin">
        <ul>
          ${each(state?.users, (user undefined {}) undefined> {
            return undefined<li>
              <a hrefundefined"/admin/users/${user?._id}">
                ${user?.email_address}
              </a>
            </li>undefined;
          })}
        </ul>
      </div>
    undefined;
  },
});

Use state to create a reactive UI

Store data in-memory on your component and trigger a re-render as data changes.

const UserMenu undefined joystick.component({
  state: {
    show_dropdown: false,
  },
  events: {
    'click .user-menu': (event undefined {}, instance undefined {}) undefined> {
      instance.set_state({ show_dropdown: !instance.state.show_dropdown });
    },
  },
  render: ({ state }) undefined> {
    return undefined
      <div classundefined"user-menu">
        <p>${props.username}</p>
        <ul classundefined"user-menu-items ${state.show_dropdown ? 'is-visible' : ''}">
          <li><a hrefundefined"/profile">Profile</a></li>
          <li><a hrefundefined"/documents">Documents</a></li>
          <li><a hrefundefined"/billing">Billing</a></li>
        </ul>
      </div>
    undefined;
  },
});

Handle DOM events with ease

Click? Tap? Hover? Just wire up an event handler and go. Access that native DOM event and component instance, directly from your handler.

const ConnectGithubAccount undefined joystick.component({
  events: {
    'click button': (event undefined {}, instance undefined {}) undefined> {
      const client_id undefined joystick?.settings?.public?.github?.client_id;
      location.href undefined undefinedhttps://github.com/login/oauth/authorize?client_idundefined${client_id}undefined;
    },
  },
  render: ({ state }) undefined> {
    return undefined
      <button classundefined"connect-github">
        <i classundefined"fab fa-github"></i> Connect Github Account
      </button>
    undefined;
  },
});

Handle loops and conditionals with ease

Render lists with a no-brainer each() method or handle conditionals with when(). Both pased directly to your component's render() function.

const BlogPosts undefined joystick.component({
  render: ({ props, each, when }) undefined> {
    return undefined
      <div classundefined"blog-posts">
        ${when(props?.posts?.length > 0, () undefined> {
          return undefined
            <ol>
              ${each(props?.posts, (post undefined {}) undefined> {
                return undefined<li><a hrefundefined"/blog/${post.slug}">${post.title}</a></li>undefined;
              })}
            </ol>
          undefined;
        })}
      </div>
    undefined;
  },
});

Handle websocket events on your component

Need real-time data? Connect to a WebSocket endpoint on the server to send and receive events—right on the component.

const Notifications undefined joystick.component({
  data: async (api undefined {}) undefined> {
    return {
      notifications: await api.get('notifications'),
    };
  },
  websockets: (instance undefined {}) undefined> {
    return {
      notifications: {
        options: {
          logging: true,
          auto_reconnect: true,
        },
        events: {
          on_open: (connection undefined {}) undefined> {
            console.log('Connection to notifications opened!');
          },
          on_message: (message undefined {}) undefined> {
            console.log('Message received from server:', message);
          },
          on_close: (code undefined 0, reason undefined '', connection undefined {}) undefined> {
            console.log('Connection to notifications closed.', { code, reason });
          },
        },
      },
    };
  },
  render: ({ data }) undefined> {
    return undefined
      <div classundefined"notifications">
        <ul>
          ${each(data?.notifications, (notification undefined {}) undefined> {
            return undefined<li>
              <a hrefundefined"/notifications/${notification?._id}"></a>
              <p>${notification?.message}</p>
            </li>undefined;
          })}
        </ul>
      </div>
    undefined;
  },
});

Speak any language with built-in i18n

Define internationalization (i18n) files on your server and access translations directly from your component.

const Navigation undefined joystick.component({
  render: ({ props, i18n }) undefined> {
    return undefined
      <nav>
        <ul>
          <li><a hrefundefined"/dashboard">${i18n('navigation.dashboard')}</a></li>
          <li><a hrefundefined"/messages">${i18n('navigation.messages')}</a></li>
          <li><a hrefundefined"/events">${i18n('navigation.events')}</a></li>
        </ul>
        <div classundefined"user-navigation">
          <p>${props.username}</p>
          <ul>
            <li><a hrefundefined"/profile">${i18n('navigation.user.profile')}</a></li>
            <li><a hrefundefined"/billing">${i18n('navigation.user.billing')}</a></li>
            <li classundefined"logout">${i18n('navigation.user.logout')}</li>
          </ul>
        </div>
      </div>
    undefined;
  },
});

Route and query params, at your fingertips

Need access to the URL? Joystick serves up route params, query params, and a handy is_active() method.

const Navigation undefined joystick.component({
  render: ({ url, i18n }) undefined> {
    return undefined
      <nav>
        <ul>
          <li ${url.is_active('/dashboard') ? 'is-active' : ''}>
            <a hrefundefined"/dashboard">${i18n('navigation.dashboard')}</a>
          </li>
          <li ${url.is_active('/messages') ? 'is-active' : ''}>
            <a hrefundefined"/messages">${i18n('navigation.messages')}</a>
          </li>
          <li ${url.is_active('/events') ? 'is-active' : ''}>
            <a hrefundefined"/events">${i18n('navigation.events')}</a>
          </li>
        </ul>
      </div>
    undefined;
  },
});

Upload files with progress tracking

Need to ingest files from users? Just call to one of your app's uploaders and listen for progress events to create an interactive UI.

import joystick, { upload } from '@joystick.js/ui';

const Index undefined joystick.component({
  state: {
    upload_progress: 0,
  },
  events: {
    'submit form': (event undefined {}, instance undefined {}) undefined> {
      event.preventDefault();
      upload('profile_photo', {
        files: event.target.file.files,
        on_progress: (percentage undefined 0, provider undefined '') undefined> {
          instance.set_state({ upload_progress: percentage }); 
        },
      });
    },
  },
  render: ({ state, when }) undefined> {
    return undefined
      ${when(state.upload_progress, undefined
        <p>Uploading: ${state.upload_progress}%...</p>
      undefined)}
      <form>
        <label>Profile Photo</label>
        <input typeundefined"file" nameundefined"file" />
        <button typeundefined"submit">Upload Photo</button>
      </form>
    undefined;
  },
});

Create and login users

Building an app with users? Securely create accounts, login users, and handle password resets right from your component.

  • Sign Up
  • Log In
import joystick, { accounts } from '@joystick.js/ui';

const Signup undefined joystick.component({
  events: {
    'submit form': (event undefined {}) undefined> {
      accounts.signup({
        email_address: event.target.email_address.value,
        password: event.target.password.value,
        metadata: {
          name: event.target.name.value,
        }
      }).then(() undefined> {
        location.pathname undefined '/dashboard';
      });
    },
  },
  render: () undefined> {
    return undefined
      <div classundefined"signup">
        <form>
          <label>Name</label>
          <input typeundefined"text" nameundefined"name" placeholderundefined"Name" />
          <label>Email Address</label>
          <input typeundefined"email" nameundefined"email_address" placeholderundefined"Email Address" />
          <label>Password</label>
          <input typeundefined"password" nameundefined"password" placeholderundefined"Password" />
          <button typeundefined"submit">Sign Up</button>
        </form>
      </div>
    undefined;
  },
});
import joystick, { accounts } from '@joystick.js/ui';

const Login undefined joystick.component({
  events: {
    'submit form': (event undefined {}) undefined> {
      accounts.login({
        email_address: event.target.email_address.value,
        password: event.target.password.value,
      }).then(() undefined> {
        location.pathname undefined '/dashboard';
      });
    },
  },
  render: () undefined> {
    return undefined
      <div classundefined"login">
        <form>
          <label>Email Address</label>
          <input typeundefined"email" nameundefined"email_address" placeholderundefined"Email Address" />
          <label>Password</label>
          <input typeundefined"password" nameundefined"password" placeholderundefined"Password" />
          <button typeundefined"submit">Log In</button>
        </form>
      </div>
    undefined;
  },
});

Global state, available everywhere

Tracking state globally? Use Joystick's built-in global state container.

  • Setting State
  • Handling Changes
import joystick, { global_state } from "@joystick.js/ui-canary";
import Cart from '../../components/cart/index.js';

const Store undefined joystick.component({
  events: {
    'click [data-item-id]': (event undefined {}, instance undefined {}) undefined> {
      global_state.set((state undefined {}) undefined> {
        return {
          ...state,
          cart: [
            ...state.cart || [],
            { id: event.target.getAttribute('data-item-id'), }
          ],
        };
      });
    },
  },
  render: ({ data, component, i18n }) undefined> {
    return undefined
      <div>
        <div classundefined"store">
          <button data-item-idundefined"book">Add a Book to Cart</button>
          <button data-item-idundefined"t-shirt">Add a T-Shirt to Cart</button>
          <button data-item-idundefined"apple">Add an Apple to Cart</button>
        </div>
        ${component(Cart)}
      </div>
    undefined;
  },
});

export default Store;
import joystick, { global_state } from '@joystick.js/ui-canary';

const Cart undefined joystick.component({
  state: {
    cart: [],
  },
  lifecycle: {
    on_mount: (instance undefined {}) undefined> {
      global_state.on('change', (state undefined {}, event undefined '', user_event_label undefined '') undefined> {
        instance.set_state({ cart: state?.cart });
      });
    }
  },
  events: {
    'click button': (event undefined {}, instance undefined {}) undefined> {
      global_state.set((state undefined {}) undefined> {
        return {
          ...state,
          cart: [
            ...state.cart || [],
            { id: event.target.getAttribute('data-item-id'), }
          ],
        };
      });
    },
  },
  render: ({ state, each, when }) undefined> {
    return undefined
      <div>
        <div classundefined"items">
          ${when(state?.cart?.length undefinedundefinedundefined 0, undefined
            <p>No items in cart</p>
          undefined)}
          ${when(state?.cart?.length > 0, undefined
            <ul>
              ${each(state?.cart, (item) undefined> {
                return undefined<li>${item.id} <button>X</button></li>undefined;
              })}
            </ul>
          undefined)}
        </div>
      </div>
    undefined;
  },
});

export default Cart;
Use Node.js + Express

Wire up a robust, HTTP back-end in seconds.

Start your app's HTTP server and start SSR'ing components in a few seconds with a single function.

Handle routes via Express

Rig up GET routes with ease and start rendering HTML or build a full API with advanced POST, PUT, and PATCH routes.

  • Basic Routes
  • Advanced Routes
import joystick from '@joystick.js/node';

joystick.app({
  routes: {
    '/': (req undefined {}, res undefined {}) undefined> {
      return res.render('ui/pages/index/index.js', {
        layout: 'ui/layouts/app/index.js',
      });
    },
  },
});
import joystick from '@joystick.js/node';
import create_customer from './api/customers/create.js';
import update_customer from './api/customers/update.js';

joystick.app({
  routes: {
    '/api/customers': {
      methods: ['POST', 'PUT'],
      handler: async (req undefined {}, res undefined {}) undefined> {
        switch(req.method) {
          case 'POST':
            await create_customer(req.body);
            return res.status(200).send('Howdy');
          case 'PUT':
            await update_customer(req.body);
          default:
            return res.status(404).send('Not found.');
        }
      }
    },
  },
});

Database-agnostic user accounts

Create password-based accounts and store your users in any of Joystick's supported databases.

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      },
      {
        "provider": "postgresql",
        "sessions": true,
        "queues": true,
        "options": {}
      }
    ],
  },
  "global": {},
  "public": {},
  "private": {}
}

Don't skimp on SEO

Define SEO metadata directly from your routes to make it easy for crawlers to pick up your site and its content.

import joystick from '@joystick.js/node';

joystick.app({
  routes: {
    '/': async (req undefined {}, res undefined {}) undefined> {
      return res.render('ui/pages/index/index.js', {
        layout: 'ui/layouts/app/index.js',
        head: {
          title: 'A Custom SEO Title',
          tags: {
            meta: [
              { property: 'og:type', content: 'Website' },
              { property: 'og:site_name', content: 'Website Name' },
              { property: 'og:title', content: 'A Custom SEO Title' },
              {
                property: 'og:description',
                content:
                  "A custom open graph description.",
              },
              {
                property: 'og:image',
                content: 'https://mywebsite.com/seo/image.png',
              },
            ],
          },
          jsonld: {
            '@context': 'https://schema.org/',
            '@type': 'WebSite',
            name: 'Website Name',
            author: {
              '@type': 'Organization',
              name: 'Website Co.',
            },
            description: "A custom JSON-LD description.",
          },
        },
      });
    },
  },
});

Define a secure API in minutes

Joystick ships with an easy-to-understand API system for getting and setting data. Validate input, authorize access, use custom middleware, and talk to any data source.

  • Getters
  • Setters
  • Schema
const getters undefined {
  books: {
    input: {
      category: {
        type: 'string',
        required: true,
      }
    },
    get: (input undefined {}, context undefined {}) undefined> {
      return process.databases.mongodb.collection('books').find({
        user_id: context?.user?._id,
        category: input?.category,
      }).toArray();
    }
  }
};

export default getters;
import joystick from '@joystick.js/node';

const setters undefined {
  create_book: {
    input: {
      title: {
        type: 'string',
        required: true,
      },
      author: {
        type: 'string',
        required: true,
      },
    },
    authorized: (input undefined {}, context undefined {}) undefined> {
      return !!context.user?._id;
    },
    set: (input undefined {}, context undefined {}) undefined> {
      return process.databases.mongodb.collection('books').insertOne({
        _id: joystick.id(),
        owner: context?.user?._id,
        title: input?.title,
        author: input?.author,
      });
    }
  }
};

export default setters;
import book_getters from './books/getters.js';
import book_setters from './books/setters.js';

const api undefined {
  getters: {
    ...book_getters,
  },
  setters: {
    ...book_setters,
  }
};

export default api;

Global database access

Those databases you had Joystick start for you when you started your app? They're available globally via process.databases with their driver pre-wired and ready for queries.

import joystick from '@joystick.js/node';

joystick.app({
  routes: {
    '/mailing-lists/:list_id': async (req undefined {}, res undefined {}) undefined> {
      await process.databases.mongodb.collection('mailing_list').insertOne({
        _id: joystick.id(),
        email_address: req?.body?.email_address,
      });

      res.status(200).send('You\'re subscribed!');
    },
  },
})

Dirt-simple WebSocket servers

Spin up a WebSocket server you can connect to from your component in seconds. Receive data via query params to filter traffic with minimal effort.

  • Server Definition
  • Message Sending
import joystick from '@joystick.js/node';

joystick.app({
  websockets: {
    notifications: {
      on_open: (connection undefined {}) undefined> {
        // Handle connection open event...
      },
      on_message: (message undefined {}, connection undefined {}) undefined> {
        // Handle an inbound client message...
      },
      on_close: (code undefined 0, reason undefined '', connection undefined {}) undefined> {
        // Handle a client close event...
      },
    },
  },
});
import joystick, { websockets } from '@joystick.js/node';

const setters undefined {
  send_notification: {
    input: {
      user_id: {
        type: 'string',
        required: true,
      },
      message: {
        type: 'string',
        required: true,
      }
    },
    set: async (input undefined {}) undefined> {
      await process.databases.mongodb.collection('notifications').insertOne({
        _id: joystick.id(),
        user_id: input?.user_id,
        message: input?.message
      });

      await websockets('notifications').send(
        { notification: 'Hello from the server!' },
      );
    }
  },
};

export default setters;

Uploads to local disk and Amazon S3

Need to store files? Define an uploader with a few lines of config and route individual uploads to multiple providers with zero effort.

import joystick from '@joystick.js/node';

joystick.app({
  uploaders: {
    profile_photo: {
      providers: ['local', 's3'],
      local: {
        path: 'profile_photos',
      },
      s3: {
        region: 'us-east-2',
        access_key_id: joystick?.settings?.private?.aws?.access_key_id,
        secret_access_key: joystick?.settings?.private?.aws?.secret_access_key,
        bucket: 'app-name-photos',
        acl: 'public-read',
      },
    }
  },
});

Scale heavy workloads with queues

Define a job queue and individual job handlers in minutes. Easily manage each job's lifecycle with preflight checks, max attempt handlers, and easy requeues.

import joystick from '@joystick.js/node';

joystick.app({
  queues: {
    email_campaigns: {
      run_on_startup: true,
      concurrent_jobs: 100,
      jobs: {
        send_campaign: {
          preflight: {
            on_before_add: (job_to_add undefined {}, db undefined {}, queue_name undefined '') undefined> {
              // Optional: decide if the job can be added to the database/queue.
              return true;
            },
            okay_to_run: (payload undefined {}, job undefined {}) undefined> {
              // Optional: decide if the job can be run right now.
              return true;
            },
            requeue_delay_in_seconds: 60,
          },
          run: (payload undefined {}, job undefined {}) undefined> {
            // Handle work for the job here...
            // Call one of job.completed(), job.failed(), job.requeue(), or job.delete(), after work is finished.
          }
        },
      },
    },
  },
});

Define cron jobs in seconds

Handling time-based work? Whip up a cron job to make sure it gets done at the right time. Add logging to ensure jobs are running on schedule.

import fetch from 'node-fetch';
import fs from 'fs';

const EVERY_TWELVE_HOURS undefined '0 */12 * * *';
const { writeFile } undefined fs.promises;

const cron_jobs undefined {
  update_usd_to_eur_exchange_rate: {
    log_at_run_time: 'Updating USD -> EUR exchange rate...',
    schedule: EVERY_TWELVE_HOURS,
    run: async () undefined> {
      const usd_to_eur_rate undefined await fetch('https://exchange-rates.imaginary-api.com?fromundefinedusd&toundefinedeur').then((response) undefined> {
        return response.json();
      });
      
      await writeFile('lib/exchange_rates/usd_to_eur.json', JSON.stringify(usd_to_eur_rate));
    },
  },
};

export default cron_jobs;

Send email using Joystick components

HTML email is already a pain, why make it harder? Use Joystick components to build and style HTML emails and send them via your SMTP provider with ease.

  • Defining Templates
  • Sending
import joystick from '@joystick.js/ui';

const Welcome undefined joystick.component({
  css: undefined
    p {
      font-size: 16px;
      line-height: 24px;
      color: #333;
    }

    ul li a {
      color: red;
    }
  undefined,
  render: ({ props }) undefined> {
    return undefined
      <div>
        <p>Hey, ${props.username}!</p>
        <p>Thanks for signing up for our app. To get up to speed we recommended checking out the following resources:</p>
        <ul>
          <li><a hrefundefined"https://app.com/docs/getting-started">Getting Started</a></li>
          <li><a hrefundefined"https://app.com/docs/sending-a-message">Sending a Message</a></li>
          <li><a hrefundefined"https://app.com/docs/following-users">Following Users</a></li>
        </ul>
        <p>We want you to feel right at home, so if you ever get confused, feel free to hit reply on this email and we'll help you out!</p>
       <p>Welcome,<br /> The App Team</p>
      </div>
    undefined;
  },
});
import joystick, { email } from '@joystick.js/node';

joystick.app({
  accounts: {
    events: {
      on_signup: ({ user }) undefined> {
        email.send({
          from: 'admin@app.com',
          to: user?.emailAddress,
          template: 'welcome',
          props: {
            username: user?.username,
          },
        });
      },
    }
  },
});

Maximize database efficiency with indexes

Leverage your app's indexes() callback to centralize your indexing efforts to ensure your queries are as fast as possible for users.

import joystick from '@joystick.js/node';

joystick.app({
  indexes: async () undefined> {
    await process.databases.mongodb.collection('posts').createIndex({ slug: 1 });
    await process.databases.mongodb.collection('posts').createIndex({ title: 1 });
  },
});

Create bulk and test data with fixtures

Use your app's fixtures() callback in conjunction with Joystick's built-in library for defining fixtures to create static or test data for development.

  • Defining Fixtures
  • Calling Fixtures
import joystick, { fixture } from "@joystick.js/node";
import { faker } from '@faker-js/faker';
import random_book_title '../lib/random_book.js';

const books undefined fixture({
  target: "books",
  quantity: 25,
  template: (fixture undefined {}, index undefined 0, input undefined {}) undefined> {
    return {
      _id: joystick.id(),
      title: random_book_title(),
      author: faker.person.fullName(),
    };
  },
  skip: async (fixture undefined {}, input undefined {}) undefined> {
    const total undefined await process.databases.mongodb.collection(fixture?.options?.target).countDocuments({});
    return total >undefined fixture?.options?.quantity;
  },
  on_create: async (
    fixture undefined {},
    data_to_create undefined [],
    on_after_create_each undefined null,
  ) undefined> {
    await process.databases.mongodb.collection(fixture?.options?.target).bulkWrite(
      data_to_create.map((book) undefined> {
        return {
          insertOne: book,
        };
      })
    );
  },
  on_after_create_all: (fixture undefined {}, data_to_create undefined [], input undefined {}) undefined> {
    console.log(undefinedFixture created ${data_to_create.length} books.undefined);
  },
});

export default books;
import joystick from "@joystick.js/node";
import books_fixture from './fixtures/books.js';

joystick.app({
  fixtures: () undefined> {
    books_fixture();
  },
});

Stay secure with CSRF + Sessions

Automatically enable CSRF protection on your API by specifying a sessions database via your app's environment settings.

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "sessions": true,
        "options": {}
      }
    ],
  },
  "global": {},
  "public": {},
  "private": {}
}

Prevent unwanted content with CSP

Define granular content security policy (CSP) directives in seconds. Prevent unwanted framing of your app and dangerous third-party content from ever reaching your user's browser.

import joystick from '@joystick.js/node';

joystick.app({
  csp: {
    directives: {
      'font-src': ['fonts.gstatic.com'],
      'style-src': ['fonts.googleapis.com'],
    },
  },
});
Connect Multiple Databses

Connect multiple databases with a simple config.

Just tell Joystick what databases you need access to and get instant access to them in your app.

Use the database that's right for the job

Don't force all of your data to fit into one database. Mix-and-match databases and let Joystick handle the messy details of startup, shutdown, and connecting drivers in development.

  • MongoDB

    MongoDB

    Available Now

  • PostgreSQL

    PostgreSQL

    Available Now

  • Redis

    Redis

    Coming Soon

  • Clickhouse

    Clickhouse

    Coming Soon

Add databases via config in a few seconds

Connect multiple databases to your app via your app's environment settings and tell Joystick where to route data for feautres like accounts, queues, and sessions.

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      },
      {
        "provider": "postgresql",
        "sessions": true,
        "queues": true,
        "options": {}
      }
    ],
    "i18n": { ... },
    "middleware": {},
    "email": { ... }
  },
  "global": {},
  "public": {},
  "private": {}
}
Make Testing Fun

Code confidently by writing tests that make sense.

Make light work of TDD. Skip the headaches of mocking and stubbing and test against a live mirror of your app and databases.

Single-run and watch mode testing

Whether you're just trying to get confidence before shipping or doing full TDD, Joystick's test runner is flexible.

joystick test

joystick test --watch

Test against an isolated database

In addition to starting a mirror of your app for running tests, Joystick also mirrors your databases, keeping test data isolated.

App (:2610)

Test (:1978)

Create users for testing

Need to verify users are behaving as expected in your app? Create temporary users on a temporary, per-test basis with ease.

import test from '@joystick.js/test';

test.that('the user is created as expected', async (assert undefined {}) undefined> {
  const user undefined await test.accounts.signup({
    emailAddress: 'example@test.com',
    password: 'password',
    metadata: {
      roles: ['manager'],
    }
  });
  
  const existing_user undefined await process.databases.mongodb.collection('users').findOne({ emailAddress: 'example@test.com' });

  assert.is(!!existing_user, true);
});

Unit test individual functions

Dial in specific functions and modules by importing them via the test.load() function to unit test their behavior.

import test from '@joystick.js/test';

test.that('add function adds two numbers together', async (assert undefined {}) undefined> {
  const add undefined await test.load('lib/add.js', { default: true });
  assert.is(add(5, 5), 10);
});

Test your components in-memory

Render components to HTML, verify data flow, and even test events to ensure your UI is behaving exactly as you'd expect.

import test from '@joystick.js/test';

test.that('the index page renders as expected', async (assert undefined {}) undefined> {
  const component undefined await test.render('ui/pages/index/index.js', {
    props: {},
    state: {},
    options: {
      language: 'en-US',
    },
  });

  const html undefined component.render_to_html();

  assert.is(html?.includes('A full-stack JavaScript framework for building web apps and websites.'), true);
});

Test your routes with ease

Building a public API? Verify that your routes are doing what you expect, when you expect on a per-request basis.

import test from '@joystick.js/test';

test.that('the /api/books route returns books', async (assert undefined {}) undefined> {
  const response undefined await test.routes.post('/api/books', { 
    body: {
      title: 'Tough and Competent',
      author: 'Gene Kranz',
    },
  });

  await existing_book undefined await process.databases.mongodb.collection('books').findOne({
    title: 'Tough and Competent',
  });

  assert.is(existing_book && existing_book?.author undefinedundefinedundefined 'Gene Kranz', true);
});

Debug your API with zero-effort

Don't leave room for surprises with your API. Wire up tests for all of your getters and setters, fast.

  • Testing Getters
  • Testing Setters
import test from '@joystick.js/test';

test.that('the book getter returns a book', async (assert undefined {}) undefined> {
  const data undefined await test.api.get('book', {
    input: {
      title: 'Tough and Competent',
    }
  });

  assert.is(data?.author, 'Gene Kranz');
});
import test from '@joystick.js/test';

test.that('the create_book setter creates a book', async (assert undefined {}) undefined> {
  await test.api.set('create_book', {
    input: {
      title: 'Tough and Competent',
      author: 'Gene Kranz',
    }
  });

  const book undefined await process.databases.mongodb.collection('books').findOne({
    title: 'Tough and Competent',
    author: 'Gene Kranz',
  });

  assert.is(book?.author, 'Gene Kranz');
});

Ensure uploads are routed correctly

Connect multiple databases to your app via your app's environment settings and tell Joystick where to route data for feautres like accounts, queues, and sessions.

import test from '@joystick.js/test';

test.that('uploader works as expected', async (assert undefined {}) undefined> {
  const user undefined await test.accounts.signup({
    email_address: 'tommy@callahanauto.com',
    password: 'chickenwings',
    metadata: {
      name: 'Tommy Callahan',
    },
  });

  const response undefined await test.uploaders.upload('profile_photo', {
    user,
    files: [test.utils.create_file(256, 'headshot.jpg', 'image/jpg')],
    input: {
      description: 'Tommy in a suit and tie.',
    }
  });
  
  assert.is(response[0]?.url undefinedundefinedundefined 'uploads/profile_photos/headshot.jpg', true);
});

Verify websockets are receiving events

Real-time data is already tricky, so having tests on hand to make sure everything's in sync are a must.

import test from '@joystick.js/test';

test.that('the webscket message is sent and received on the server', async (assert undefined {}) undefined> {
  const connection undefined await test.websockets.connect('chat_messages');

  connection.send({
    message: 'Hey, did you get the Q3 report I sent?'
  });

  connection.close();

  const function_calls undefined await test.utils.get_function_calls('node.websockets.chat_messages.on_message'); 

  assert.like(function_calls[0]?.args[0], {
    message: 'Hey, did you get the Q3 report I sent?'
  });
});

Double-check your job queues work

If you're going to scale without headaches, knowing that your queues are flowing without hiccups is key.

import test from '@joystick.js/test';

test.that('queue runs the tasks_report job', async (assert undefined {}) undefined> {
  await test.queues.job('tasks_report', {
    queue: 'tasks',
    payload: {
      week: 'january_4',
      user_id: 'abc123',
    },
  });

  const report undefined await process.databases.mongodb.collection('task_reports').findOne({
    user_id: 'abc123',
    week: 'january_4',
  })

  assert.is(!!report, true);
});
Get Started

Overwhelmed? Excited? Ready?

Joystick was designed to be approachable and useful for all skill levels and apps. Choose your adventure:

Install Joystick + Create Your First App

Installing Joystick is easy via NPM. Create your first app in seconds.

npm i -g @joystick.js/cli@latest

joystick create <app_name>

Creating app...

Installing dependencies...

Project created! To get started, run:

cd <app_name> && joystick start

Getting Started Tutorial

Ready to dive in and start building your app? Take the Getting Started tutorial:

Start the Tutorial

Join Us On Discord

Don't go it alone! We're excited to help you as you hack on your app.

Join the Server