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 = joystick.component({
  render: ({ props, each }) => {
    return `
      <div class="video-player">
        <video>
          ${each(props.sources, (source) => {
            return `<source src="${source.src}" type="${source.type}" />`
          })}
        </video>
      </div>
    `;
  },
});

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 = joystick.component({
  css: `
    .video-player {
      border: 1px solid #000;
      padding: 20px;
      border-radius: 5px;
    }
  `,
  render: ({ props, each }) => {
    return `
      <div class="video-player">
        ...
      </div>
    `;
  },
});
const VideoPlayer = joystick.component({
  css: {
    min: {
      width: {
        0: `
          .video-player {
            border: 1px solid #000;
            padding: 20px;
            border-radius: 5px;
          }
        `,
        768: `
          .video-player {
            padding: 30px;
          }
        `
      }
    }
  },
  render: ({ props, each }) => {
    return `
      <div class="video-player">
        ...
      </div>
    `;
  },
});

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 = joystick.component({
  data: async (api = {}) => {
    return {
      users: await api.get('admin_users'),
    };
  },
  render: ({ data, each }) => {
    return `
      <div class="user-admin">
        <ul>
          ${each(data?.users, (user = {}) => {
            return `<li>
              <a href="/admin/users/${user?._id}">
                ${user?.email_address}
              </a>
            </li>`;
          })}
        </ul>
      </div>
    `;
  },
});
import { get } from '@joystick.js/ui';

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

Use state to create a reactive UI

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

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

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 = joystick.component({
  events: {
    'click button': (event = {}, instance = {}) => {
      const client_id = joystick?.settings?.public?.github?.client_id;
      location.href = `https://github.com/login/oauth/authorize?client_id=${client_id}`;
    },
  },
  render: ({ state }) => {
    return `
      <button class="connect-github">
        <i class="fab fa-github"></i> Connect Github Account
      </button>
    `;
  },
});

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 = joystick.component({
  render: ({ props, each, when }) => {
    return `
      <div class="blog-posts">
        ${when(props?.posts?.length > 0, () => {
          return `
            <ol>
              ${each(props?.posts, (post = {}) => {
                return `<li><a href="/blog/${post.slug}">${post.title}</a></li>`;
              })}
            </ol>
          `;
        })}
      </div>
    `;
  },
});

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 = joystick.component({
  data: async (api = {}) => {
    return {
      notifications: await api.get('notifications'),
    };
  },
  websockets: (instance = {}) => {
    return {
      notifications: {
        options: {
          logging: true,
          auto_reconnect: true,
        },
        events: {
          on_open: (connection = {}) => {
            console.log('Connection to notifications opened!');
          },
          on_message: (message = {}) => {
            console.log('Message received from server:', message);
          },
          on_close: (code = 0, reason = '', connection = {}) => {
            console.log('Connection to notifications closed.', { code, reason });
          },
        },
      },
    };
  },
  render: ({ data }) => {
    return `
      <div class="notifications">
        <ul>
          ${each(data?.notifications, (notification = {}) => {
            return `<li>
              <a href="/notifications/${notification?._id}"></a>
              <p>${notification?.message}</p>
            </li>`;
          })}
        </ul>
      </div>
    `;
  },
});

Speak any language with built-in i18n

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

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

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 = joystick.component({
  render: ({ url, i18n }) => {
    return `
      <nav>
        <ul>
          <li ${url.is_active('/dashboard') ? 'is-active' : ''}>
            <a href="/dashboard">${i18n('navigation.dashboard')}</a>
          </li>
          <li ${url.is_active('/messages') ? 'is-active' : ''}>
            <a href="/messages">${i18n('navigation.messages')}</a>
          </li>
          <li ${url.is_active('/events') ? 'is-active' : ''}>
            <a href="/events">${i18n('navigation.events')}</a>
          </li>
        </ul>
      </div>
    `;
  },
});

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 = joystick.component({
  state: {
    upload_progress: 0,
  },
  events: {
    'submit form': (event = {}, instance = {}) => {
      event.preventDefault();
      upload('profile_photo', {
        files: event.target.file.files,
        on_progress: (percentage = 0, provider = '') => {
          instance.set_state({ upload_progress: percentage }); 
        },
      });
    },
  },
  render: ({ state, when }) => {
    return `
      ${when(state.upload_progress, `
        <p>Uploading: ${state.upload_progress}%...</p>
      `)}
      <form>
        <label>Profile Photo</label>
        <input type="file" name="file" />
        <button type="submit">Upload Photo</button>
      </form>
    `;
  },
});

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 = joystick.component({
  events: {
    'submit form': (event = {}) => {
      accounts.signup({
        email_address: event.target.email_address.value,
        password: event.target.password.value,
        metadata: {
          name: event.target.name.value,
        }
      }).then(() => {
        location.pathname = '/dashboard';
      });
    },
  },
  render: () => {
    return `
      <div class="signup">
        <form>
          <label>Name</label>
          <input type="text" name="name" placeholder="Name" />
          <label>Email Address</label>
          <input type="email" name="email_address" placeholder="Email Address" />
          <label>Password</label>
          <input type="password" name="password" placeholder="Password" />
          <button type="submit">Sign Up</button>
        </form>
      </div>
    `;
  },
});
import joystick, { accounts } from '@joystick.js/ui';

const Login = joystick.component({
  events: {
    'submit form': (event = {}) => {
      accounts.login({
        email_address: event.target.email_address.value,
        password: event.target.password.value,
      }).then(() => {
        location.pathname = '/dashboard';
      });
    },
  },
  render: () => {
    return `
      <div class="login">
        <form>
          <label>Email Address</label>
          <input type="email" name="email_address" placeholder="Email Address" />
          <label>Password</label>
          <input type="password" name="password" placeholder="Password" />
          <button type="submit">Log In</button>
        </form>
      </div>
    `;
  },
});

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

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

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

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 = {}, res = {}) => {
      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 = {}, res = {}) => {
        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 = {}, res = {}) => {
      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 = {
  books: {
    input: {
      category: {
        type: 'string',
        required: true,
      }
    },
    get: (input = {}, context = {}) => {
      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 = {
  create_book: {
    input: {
      title: {
        type: 'string',
        required: true,
      },
      author: {
        type: 'string',
        required: true,
      },
    },
    authorized: (input = {}, context = {}) => {
      return !!context.user?._id;
    },
    set: (input = {}, context = {}) => {
      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 = {
  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 = {}, res = {}) => {
      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 = {}) => {
        // Handle connection open event...
      },
      on_message: (message = {}, connection = {}) => {
        // Handle an inbound client message...
      },
      on_close: (code = 0, reason = '', connection = {}) => {
        // Handle a client close event...
      },
    },
  },
});
import joystick, { websockets } from '@joystick.js/node';

const setters = {
  send_notification: {
    input: {
      user_id: {
        type: 'string',
        required: true,
      },
      message: {
        type: 'string',
        required: true,
      }
    },
    set: async (input = {}) => {
      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 = {}, db = {}, queue_name = '') => {
              // Optional: decide if the job can be added to the database/queue.
              return true;
            },
            okay_to_run: (payload = {}, job = {}) => {
              // Optional: decide if the job can be run right now.
              return true;
            },
            requeue_delay_in_seconds: 60,
          },
          run: (payload = {}, job = {}) => {
            // 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 = '0 */12 * * *';
const { writeFile } = fs.promises;

const cron_jobs = {
  update_usd_to_eur_exchange_rate: {
    log_at_run_time: 'Updating USD -> EUR exchange rate...',
    schedule: EVERY_TWELVE_HOURS,
    run: async () => {
      const usd_to_eur_rate = await fetch('https://exchange-rates.imaginary-api.com?from=usd&to=eur').then((response) => {
        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 = joystick.component({
  css: `
    p {
      font-size: 16px;
      line-height: 24px;
      color: #333;
    }

    ul li a {
      color: red;
    }
  `,
  render: ({ props }) => {
    return `
      <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 href="https://app.com/docs/getting-started">Getting Started</a></li>
          <li><a href="https://app.com/docs/sending-a-message">Sending a Message</a></li>
          <li><a href="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>
    `;
  },
});
import joystick, { email } from '@joystick.js/node';

joystick.app({
  accounts: {
    events: {
      on_signup: ({ user }) => {
        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 () => {
    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 = fixture({
  target: "books",
  quantity: 25,
  template: (fixture = {}, index = 0, input = {}) => {
    return {
      _id: joystick.id(),
      title: random_book_title(),
      author: faker.person.fullName(),
    };
  },
  skip: async (fixture = {}, input = {}) => {
    const total = await process.databases.mongodb.collection(fixture?.options?.target).countDocuments({});
    return total >= fixture?.options?.quantity;
  },
  on_create: async (
    fixture = {},
    data_to_create = [],
    on_after_create_each = null,
  ) => {
    await process.databases.mongodb.collection(fixture?.options?.target).bulkWrite(
      data_to_create.map((book) => {
        return {
          insertOne: book,
        };
      })
    );
  },
  on_after_create_all: (fixture = {}, data_to_create = [], input = {}) => {
    console.log(`Fixture created ${data_to_create.length} books.`);
  },
});

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

joystick.app({
  fixtures: () => {
    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 = {}) => {
  const user = await test.accounts.signup({
    emailAddress: 'example@test.com',
    password: 'password',
    metadata: {
      roles: ['manager'],
    }
  });
  
  const existing_user = 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 = {}) => {
  const add = 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 = {}) => {
  const component = await test.render('ui/pages/index/index.js', {
    props: {},
    state: {},
    options: {
      language: 'en-US',
    },
  });

  const html = 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 = {}) => {
  const response = await test.routes.post('/api/books', { 
    body: {
      title: 'Tough and Competent',
      author: 'Gene Kranz',
    },
  });

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

  assert.is(existing_book && existing_book?.author === '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 = {}) => {
  const data = 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 = {}) => {
  await test.api.set('create_book', {
    input: {
      title: 'Tough and Competent',
      author: 'Gene Kranz',
    }
  });

  const book = 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 = {}) => {
  const user = await test.accounts.signup({
    email_address: 'tommy@callahanauto.com',
    password: 'chickenwings',
    metadata: {
      name: 'Tommy Callahan',
    },
  });

  const response = 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 === '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 = {}) => {
  const connection = await test.websockets.connect('chat_messages');

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

  connection.close();

  const function_calls = 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 = {}) => {
  await test.queues.job('tasks_report', {
    queue: 'tasks',
    payload: {
      week: 'january_4',
      user_id: 'abc123',
    },
  });

  const report = 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