tutorial // Nov 05, 2021

How to Implement an API Using Getters and Setters in Joystick

How to define an HTTP API using getters and setters in Joystick and call to those getters and setters from your user interface via the get() and set() methods in @joystick.js/ui.

How to Implement an API Using Getters and Setters in Joystick

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Defining and Loading a schema in Joystick

In a Joystick app, the entirety of your API is referred to as a schema. A schema is a JavaScript object containing two properties: getters and setters, both of which are set to their own objects.

As their names imply, the getters object contain your API endpoints for getting data or reading data from a database and the setters object contains your API endpoints for setting or creating, updating, and deleting data.

To begin, we're going to wire up a basic schema without any getters or setters defined and load it into our app via the node.app() function which starts up the server for our app.

/api/index.js

export default {
  getters: {},
  setters: {},
};

We want to define our schema in the index.js file under the /api directory at the root of our project. Again, your schema is just an object with a getters and setters property, each set to an object. Because we intend to import this inside of our /index.server.js file next, we use an export default statement.

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Here in our /index.server.js file, we've imported our api file up at the top. Notice that we use the word api as this is the name the node.app() function expects us to pass our schema as (again, api and schema are used interchangeably and a good phrase to memorize is "the API is defined by the schema"). Because we did an export default back in /api/index.js, here, we omit curly braces (used for creating named exports).

On the options object passed to node.app(), we've set api as a property, using JavaScript short-hand to automatically assign the value of the api value we've imported up top as the value of the api property on our node.app() options object. So it's clear, this is equivalent to saying:

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api: api,
  routes: { ... },
});

That's it for defining our base schema and loading it as our API. Now, when our app starts up (or in this case, restarts as we already started our app above), the schema will be loaded and available for requests.

Next, we're going to build out our schema by adding a getter endpoint.

Defining a getter endpoint

Like we hinted at earlier, in a Joystick app, there are two types of API endpoints: getters and setters. Getters are HTTP endpoints that anticipate an HTTP GET request being sent to them. What makes getters special is three-fold:

  1. Getters can optionally have input validation to help you validate that the input values passed to a getter when it's called are correct.
  2. When called, getters can be passed an output definition which allows you to customize the return value of a getter and describes what values you expect in return to the call.
  3. If available, getters are automatically given access to any databases you've loaded in your app as well as the logged in user if one exists.

What's nice is that this functionality is not just accessible via Joystick's built-in methods for calling getters (we'll look at these later)—they're also defined as plain HTTP endpoints like: http://localhost:2600/api/_getters/posts or http://localhost:2600/api/_getters/name-with-spaces. This means that you can use a regular fetch() function to access your getters or, access your API endpoints outside of Joystick without any special code.

/api/posts/getters.js

export default {
  posts: {
    input: {},
    get: () => {
      // We'll respond to the getter request here...
    },
  },
};

To keep our API organized, we're going to split up our getter definitions into their own file (we could technically write them directly into our schema but this is a bad habit that can create messes as our schema grows). Above, under our existing /api folder at the root of our app, we've created another folder posts and within that, a getters.js file.

The idea here is that our API is made up of "resources" or "topics." Each resource or topic has multiple endpoints related to itself. For example, here, our resource is posts which will have getter endpoints related to posts and, later, setter endpoints related to posts. Using this pattern, we keep our code easy to navigate and more importantly: easy to maintain long-term.

Just like we saw with our schema earlier, defining an individual getter only requires writing a JavaScript object. Here, we export default an object to which all of our posts-related getters will be assigned. Each getter is defined as a property on that object (e.g., posts) assigned to an object with two properties of its own: input and get().

input is where we define the optional validation for any inputs passed to our getter when it's called. get() is a function where we can perform whatever work is necessary to respond to the getter request (i.e., get the requested data from some data source). The get() function is technically open-ended. While typically we'd want to make calls to a database inside of the function, Joystick doesn't care where your data comes from, just that you return it from the function.

/api/posts/getters.js

export default {
  posts: {
    input: {
      category: {
        type: "string",
        optional: true,
      },
    },
    get: (input, context) => {
      // We'll respond to the getter request here...
    },
  },
};

Expanding our posts getter slightly, now, we're adding in some validation for the inputs we anticipate when our getter is called. Validation is defined using Joystick's built-in validation library. The library takes in an object like the one we see being passed to input above and compares it to the input value we receive when our getter is called.

Note: Validation on getters and setters is optional, though highly recommended to help with the elimination of buggy code.

On that object, we define properties with a name identical to the name of the property on the input we're passing with our getter request. For example, assuming we sent an object like this with our request:

Example input object

{
  category: 1234
}

We'd look for a matching category property on our validation object—known as a field—to see if it has a validator assigned to it (the name we use for the object assigned to the properties on our validation). If it does, we check to see if the value passed with the request conforms to the expectations of the rules on the validator.

YLdxygM33J58HBeM/86MxdehYTE0X2kKj.0
Diagram of a validation object in Joystick.

What's cool about this validation is that it can be nested indefinitely to fit the structure of your input object. You can even validate nested objects and arrays of objects making it incredibly flexible. For our needs here, we're keeping things simple and focusing on a single field for now category which we want to validate has a value equal to a typeof string if it exists (if because the field is marked as optional).

In the example above, notice that we're intentionally passing category as an integer in our example call, not a string. This is to make the point that when our getter is called, the validation will fail and stop the request because the validation expects the category field to contain a string, not an integer.

/api/posts/getters.js

export default {
  posts: {
    input: {
      category: {
        type: "string",
        optional: true,
      },
    },
    get: (input, context) => {
      const query = {};

      if (input.category) {
        query.category = input.category;
      }

      return context.mongodb.collection('posts').find(query).toArray();
    },
  },
};

Next, with our validation set, we want to wire up our get() function. Remember, this is the function that is called and is expected to return the data we're trying to get assuming the data we've passed for input makes it through the validation step.

Our get() function takes two arguments: input the validated input object passed with the getter request and context. context is an object containing a few different things:

  • context.req the inbound HTTP request given to us by the Express.js route the getter is defined as.
  • context.res the inbound HTTP response given to us by the Express.js route the getter is defined as.
  • context.user the logged in user for the app (if available).
  • context.<db> where <db> is the name of one of the databases loaded in your app (e.g., context.mongodb).

Focusing on the body of our get() function, remember: we're defining a getter called posts so we expect our getter to return some posts.

To do that, we anticipate a connection to MongoDB being defined as context.mongodb (this is the default database that Joystick automatically starts up when you run a freshly created app with joystick start).

Before we make use of it, first, we create a variable query that will act as the "base" query we want to pass to MongoDB (an empty object means "all" in MongoDB's query language). If input.category is defined (remember, it's optional so it may not be present in input), we want to set the passed category on the query object. Assuming we passed "tutorials" for input.category, we'd expect to get something like this for query:

{ category: "tutorials" }

With our query defined, next, we call to the MongoDB driver and run our query. This may seem strange. When it comes to databases, Joystick does nothing more than start the database on your local machine and make a connection using that database's Node.js driver. In other words, everything after context.mongodb here is "just how the MongoDB driver works in Node." Joystick doesn't modify this—it makes the connection to the database and sets it on context.mongodb. That's it.

What we expect in return from this line is a JavaScript array of objects with each object representing a post that's defined in the database.

That's it for defining a getter! Next, we'll take a look at defining a setter (following a near identical pattern to what we learned above) and then learn how to assign that setter and the getter we just defined above back to our schema.

Defining a setter endpoint

Just like we did above, we want to split our setter definitions off into their own file. Again, since we're working on the posts resource (or "topic" if you prefer), we're going to stick to the /api/posts folder, however this time, we're going to create a /api/posts/setters.js file:

/api/posts/setters.js

export default {
  createPost: {
    input: {
      title: {
        type: "string",
        required: true,
      },
      category: {
        type: "string",
        required: true,
      },
      body: {
        type: "string",
        required: true,
      },
      tags: {
        type: "array",
        optional: true,
        element: {
          type: "string"
        }
      },
    },
    set: (input, context) => {
      return context.mongodb.collection('posts').insertOne({
        _id: joystick.id(),
        ...input
      });
    },
  },
};

Same exact conventions at play. The big difference here is that we're using a different name for the property we set on our exported object (for our getter we used the name posts, now we're using the name createPost for our setter) and the get() function on that property's value has been changed to set().

Everything else is the same in terms of behavior and expectations. Technically speaking, if we wanted to, we could "get" instead of "set" some data. The name of the set() function here is suggestive but not technically limited in any way. Setters behave identical to getters in that they take in some input, pass it through some validation (if defined), and then hand off that input alongside the context to a function.

Again, that function is open-ended, just like it was for the get() function on our getter. You can call to any code you like here—the naming is just a convention to help organize your API.

Looking at our validation, the big difference is that we've added more fields and we've made use of the "array" type rule for the tags field. Notice that when we've set type to "array," we can additionally pass an element field set to a nested Joystick validation object. Remember: Joystick validation can be nested indefinitely.

For our set() function, just like we saw before, we're accessing the MongoDB driver assigned to context.mongodb. This time, however, we're calling to the posts collection's insertOne method. To that method, we're passing an object we're creating which is a combination of the input value (we use the JavaScript spread operator to "unpack" the contents onto the object we're passing to .insertOne()) and an _id field.

That field is being set to a call to joystick.id(). Behind the scenes, Joystick exposes a global variable on the server called joystick which has an .id() method for generating random hex string IDs of n length (the default being 16 characters) like this: FYIlLyqzTBJdGPzz.

That does it for our setters. Next, let's add our getters and setters to the schema we set up earlier in the tutorial.

Assigning our getters and setters back to the schema

Recall that earlier we defined our base schema and added it to the node.app() options object as api. That schema, though, didn't have any getters or setters defined on it—just empty objects for each. Real quick, let's pull in the /api/posts/getters.js file and /api/posts/setters.js file we just created and set them on the schema.

/api/index.js

import postGetters from './posts/getters';
import postSetters from './posts/setters';

export default {
  getters: {
    ...postGetters,
  },
  setters: {
    ...postSetters,
  },
};

Simple. Here, all we're doing to add our getters and setters to the schema is to import the objects we exported from each file and then, in the appropriate getters or setters object, use the JavaScript spread operator ... to "unpack" those objects onto their parent object. Here, we use a naming convention of the singular form of our resource/topic name followed by either "getters" or "setters" in camel case.

That's it. To wrap up, let's take a look at how to actually call our getters and setters in the app.

Calling to getters and setters via @joystick.js/ui

As a full-stack JavaScript framework, Joystick combines together our front-end and back-end conveniently into a single app. Now, we're going to move away from the server-side of our app and focus on the client (browser). When we ran joystick create earlier, Joystick gave us an example page component rendered to the / index route of our app in index.server.js (you may have spotted this when we were wiring up the API). Let's open up that page component now in /ui/pages/index/index.js.

/ui/pages/index/index.js

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

const Index = ui.component({
  methods: {
    handleLogHello: () => {
      console.log("Hello!");
    },
  },
  events: {
    "click .say-hello": (event, 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;

Inside of this file we have an example Joystick component created using the @joystick.js/ui package (the companion to the @joystick.js/node package we saw earlier on the server). @joystick.js/ui is a library for creating user interface components using pure HTML, CSS, and JavaScript.

Want to learn more about writing Joystick components? Check out this tutorial that goes in-depth on defining your own.

The bulk of the code above isn't terribly important for us right now. What we're going to do now is modify this component to render two things:

  1. A form for creating a new post.
  2. A way to display posts that we've retrieved via our posts getter endpoint.

/ui/pages/index/index.js

import ui, { get, set } from "@joystick.js/ui";

const Index = ui.component({
  state: {
    posts: [],
  },  
  lifecycle: {
    onMount: (component) => {
      component.methods.handleFetchPosts();
    },
  },
  methods: {
    handleFetchPosts: async (component) => {
      const posts = await get('posts', {
        input: {
          category: "opinion",
        },
        output: [
          'title',
          'body'
        ],
      });

      component.setState({posts});
    },
  },
  events: {
    "submit form": (event, component) => {
      event.preventDefault();

      set('createPost', {
        input: {
          title: event.target.title.value,
          category: event.target.category.value,
          body: event.target.body.value,
          tags: event.target.tags.value.split(',').map((tag) => tag.trim()),
        },
      }).then(() => {
        event.target.reset();
        component.methods.handleFetchPosts();
      });
    },
  },
  css: `
    ul {
      list-style: none;
      padding: 0;
      margin: 0 0 20px;
    }

    li {
      border: 1px solid #eee;
      padding: 20px;
    }

    li strong span {
      font-weight: normal;
      color: #aaa;
    }
  `,
  render: ({ state, each }) => {
    return `
      <div>
        <div class="posts">
          <h4>Posts</h4>
          <ul>
            ${each(state.posts, (post) => {
              return `
                <li>
                  <strong>${post.title} <span>${post.category}</span></strong>
                  <p>${post.body}</p>
                </li>
              `;
            })}
          </ul>
        </div>

        <form>
          <label for="title">Title</label><br />
          <input name="title" placeholder="title" />

          <br />

          <label for="category">Category</label><br />
          <select name="category">
            <option value="tutorials">Tutorials</option>
            <option value="opinion">Opinion</option>
            <option value="meta">Meta</option>
          </select>

          <br />

          <label for="body">Body</label><br />
          <textarea name="body"></textarea>

          <br />

          <label for="tags">Tags</label><br />
          <input name="tags" placeholder="tag1,tag2,tag3" />
          
          <br />

          <button type="submit">Create Post</button>
        </form>
      </div>
    `;
  },
});

export default Index;

Keeping the skeleton from the existing component, here, we're swapping out what's being rendered and the core functionality of the component. This is purposeful. Re-using the /ui/pages/index/index.js component was to avoid the need for wiring up a brand new component and route and keeping us focused on our getters and setters.

Looking at the code here, the most important part is down in the render() function. When building a component with @joystick.js/ui, we return a string of HTML from the render() function using backticks. This allows us to take advantage of JavaScript's string interpolation (also known as "template literals") to dynamically "inject" values into the HTML in our string.

Behind the scenes, Joystick takes the HTML with our injected values and renders it to the browser. In our code here, to demo our getters and setters in action, we want to render two things: a list of existing posts (retrieved from our getter) from the database and a form for adding new posts (who's contents are relayed via our setter).

Because we don't have any posts in our database yet, next, we want to look at the events property set on the object we're passing to ui.component(). This is where we define JavaScript DOM events in a Joystick component. Each event we want to listen for is assigned to the object we pass to events. We create a listener by setting the key or property name as a string containing first the type of DOM event we want to listen for (in our example, submit) and the element we want to listen for that event on (in our example, form).

To that property, we assign a function that's called whenever that event occurs in the browser/DOM. For our example, we want to call our setter createPost on the server whenever this event takes place. To call it, up top we've added a named import (denoted by the curly braces) for the set() function that's included in @joystick.js/ui. This is a wrapper function around the JavaScript fetch() method that's built-in to browsers for performing HTTP requests.

It gives us a simple API for performing our request. It takes in the name of the setter we want to call as a string for its first argument, followed by an options object. On that options object, here, we're passing the values from our form. We do this by accessing the DOM event object passed to our function by Joystick.

Because Joystick is giving us access to the native DOM event, we can access the value of our inputs directly by saying event.target.<field>.value where event.target refers to the <form></form> element where the submit event was received and <field>.value equals the value of the input with a name attribute equal to <field> in our rendered HTML.

So that's clear, if we had an input like <input name="pizza" /> in our HTML, we'd write something like event.target.pizza.value.

With that, our setter is ready to be called. Remember: all we're doing on the server is handing off the validated input to MongoDB to put into our posts collection in the database.

After our setter is called, we're ready to move onto the next step: fetching our posts from the database.

Because we expect the set() method imported from @joystick.js/ui to return a JavaScript Promise, on the end of our call to that function we chain a .then() method, passing a callback function we'd like to run after the setter request is complete.

Inside, we call to the .reset() method on our form (reusing the event.target passed to our DOM event listener) to clear out the field and then, call to a custom method defined on our component handleFetchPosts(). We can access this because all DOM event listeners defined on the events object of a component receive the DOM event as the first argument and the entire component instance as the second argument.

/ui/pages/index/index.js

import ui, { get, set } from "@joystick.js/ui";

const Index = ui.component({
  state: {
    posts: [],
  },  
  lifecycle: {
    onMount: (component) => {
      component.methods.handleFetchPosts();
    },
  },
  methods: {
    handleFetchPosts: async (component) => {
      const posts = await get('posts', {
        input: {
          category: "opinion",
        },
        output: [
          'title',
          'body'
        ],
      });

      component.setState({ posts });
    },
  },
  events: {
    "submit form": (event, component) => {
      event.preventDefault();

      set('createPost', { ... }).then(() => {
        document.querySelector('form').reset();
        component.methods.handleFetchPosts();
      });
    },
  },
  css: `...`,
  render: ({ state, each }) => {
    return `
      <div>
        <div class="posts">
          <h4>Posts</h4>
          <ul>
            ${each(state.posts, (post) => {
              return `
                <li>
                  <strong>${post.title} <span>${post.category}</span></strong>
                  <p>${post.body}</p>
                </li>
              `;
            })}
          </ul>
        </div>

        <form>
          ...
          <button type="submit">Create Post</button>
        </form>
      </div>
    `;
  },
});

export default Index;

The methods object assigned to a Joystick component contains miscellaneous functions that we want to call in relation to our component. These methods can be accessed from anywhere in our component via the component instance (passed to all functions in a Joystick component).

Like we just saw in the .then() callback of our set() call in events, we can call to a method directly by writing component.methods.<methodName>.

For our needs, we want to wire up a method that calls our posts getter on the server and retrieves our data. Similar to how we called our setter via set(), a sibling method for getters is also exported from @joystick.js/ui called get().

Predictably, get() takes in the name of the getter we want to call as a string for its first argument and then an options object as its second argument. Remember that earlier when wiring up our getter endpoint we anticipated a possible category value being passed for our input. In our example here, we're passing "opinion" as a category name to say "when you run this getter, only return posts with a category field equal to opinion."

If we look down in our render() function, we can use one of three categories here: tutorials, meta, or opinion.

In addition to our input, a unique feature of getters and setters in Joystick is the output option (known as SelectiveFetch in Joystick). Output allows you to pass specific fields on an object or array of objects returned from a getter to customize its output. This makes it possible to reuse a getter in multiple places, tailoring the output to the needs of your UI. To demonstrate this here, we're passing two of the four fields defined on each of our posts title and body.

On the server, before Joystick sends our data back, it will pass it through the output array and remove any data that you didn't ask for. Here, because we pass title and body in our array, we're saying "on each post object, only give me back the title and body of the post, discarding everything else." If we comment the output option out and re-run our getter, we'll see that all fields show up as opposed to only what we passed in output.

Just like with the set() method we saw previously, we expect get() to return a JavaScript Promise. To show off a different way of handling the response from get() (you could use the same pattern with set()), we use the JavaScript async/await pattern to skip the need for a .then() callback.

This works by assigning the keyword async to the parent function where the await keyword is going to be used. We put the await keyword here in front of our call to get() to say "wait until this getter responds and then assign the response value to the variable posts.

With that variable (assuming it contains an array of posts like we returned from our getter), we take in the component instance (for methods, this is automatically passed as the last possible argument—in this case, it's first because we're not passing any other arguments to handleFetchPosts() when we call it) and utilize its .setState() method to set the state.posts value on the component to the array we received from our getter.

We can see that the state object at the top of our component is given a default posts value set to an empty array. When our getter runs, assuming we return data from our getter, we'll automatically populate this array. Down in our render() method, we use the each() render function in Joystick to say "for each of the posts in state.posts, call this function which renders a string of HTML and receives the current post (or value) we're looping over."

In turn, we expect an <li></li> tag to be rendered on screen with each post's title, category and body injected into it.

One last note before we give this a test. Notice that we've also added an object lifecycle to our component. This object allows us to define functions that are called at different stages of our component's "life cycle," or, "what the component is currently doing." There are three lifecycle methods in Joystick: onBeforeMount, onMount, and onBeforeUnmount.

Here, we're using onMount to say "when this component renders, call to the handleFetchPosts() method in our methods object." What we expect is that when our component renders on screen for the first time, it will go and fetch any existing posts and put them on state, triggering a re-render in our HTML and showing the list of posts on screen. Each time we add a new post, too, we expect the same behavior (meaning posts show up on screen as soon as they're added to the database).

That's it! Let's give this a test run and see how it works.

Wrapping up

In this tutorial, we learned how to create a simple API using Joystick's getters and setters. We learned how to create a getter and setter and then load them into our schema and attach that schema to our app as our API. We also learned how to call to getters and setters in the browser, using the get() and set() methods included in the @joystick.js/ui library.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode