Back-end
13 minute read

ActiveResource.js: Building a Powerful JavaScript SDK For Your JSON API, Fast

Former CTO of Humanity Rising and an experienced full-stack engineer, Nick enjoys making tech accessible and being active in the FOSS scene.

Your company just launched its API and now wants to build a community of users around it. You know that most of your customers will be working in JavaScript, because the services your API provide make it easier for customers to build web applications instead of writing everything themselves—Twilio is a good example of this.

You also know that as simple as your RESTful API may be, users are going to want to drop in a JavaScript package that will do all the heavy lifting for them. They won’t want to learn your API and build each request they need themselves.

So you’re building a library around your API. Or maybe you are just writing a state management system for a web application that interacts with your own internal API.

Either way, you don’t want to repeat yourself over and over each time you CRUD one of your API resources, or worse, CRUD a resource related to those resources. This isn’t good for managing a growing SDK over the long run, nor is it a good use of your time.

Instead, you can use ActiveResource.js, a JavaScript ORM system for interacting with APIs. I created it to fill a need we had on a project: to create a JavaScript SDK in as few lines as possible. This enabled maximum efficiency for us and for our developer community.

It is based on the principles behind Ruby on Rails’ simple ActiveRecord ORM.

JavaScript SDK Principles

There are two Ruby on Rails ideas that guided the design of ActiveResource.js:

  1. “Convention over configuration:” Make some assumptions about the nature of the API’s endpoints. For example, if you have a Product resource, it corresponds to the /products endpoint. That way time is not spent repeatedly configuring each of your SDK’s requests to your API. Developers can add new API resources with complicated CRUD queries to your growing SDK in minutes, not hours.
  2. “Exalt beautiful code:” Rails’ creator DHH said it best—there is just something great about beautiful code for its own sake. ActiveResource.js wraps sometimes ugly requests in a beautiful exterior. You no longer have to write custom code to add filters and pagination and include relationships nested upon relationships to GET requests. Nor do you have to construct POST and PATCH requests that take changes to an object’s properties and send them to the server for updating. Instead, just call a method on an ActiveResource: No more playing around with JSON to get the request you want, only to have to do it again for the next one.

Before We Start

It is important to note that at the time of this writing, ActiveResource.js works only with APIs written according to the JSON:API standard.

If you are unfamiliar with JSON:API and want to follow along, there are many good libraries for creating a JSON:API server.

That said, ActiveResource.js is more a DSL than a wrapper for one particular API standard. The interface that it uses to interact with your API can be extended, so future articles could cover how to use ActiveResource.js with your custom API.

Setting Things Up

To begin, install active-resource in your project:

yarn add active-resource

The first step is to create a ResourceLibrary for your API. I’m going to put all my ActiveResources in the src/resources folder:

// /src/resources/library.js

import { createResourceLibrary } from 'active-resource';

const library = createResourceLibrary('http://example.com/api/v1');

export default library;

The only required parameter to createResourceLibrary is the root URL of your API.

What We’ll Create

We’re going to create a JavaScript SDK library for a content management system API. That means there will be users, posts, comments, and notifications.

Users will be able to read, create, and edit posts; read, add, and delete comments (to posts, or to other comments), and receive notifications of new posts and comments.

I’m not going to use any specific library for managing the view (React, Angular, etc.) or the state (Redux, etc.), instead abstracting the tutorial to interact only with your API through ActiveResources.

The First Resource: Users

We’re going to start by creating a User resource to manage the users of the CMS.

First, we create a User resource class with some attributes:

// /src/resources/User.js

import library from './library';

class User extends library.Base {
  static define() {
    this.attributes('email', 'userName', 'admin');
  }
}

export default library.createResource(User);

Let’s assume for now that you have an authentication endpoint that, once a user submits their email and password, returns an access token and the ID of the user. This endpoint is managed by some function requestToken. Once you get the authenticated user ID, you want to load all of the user’s data:

import library from '/src/resources/library';
import User from '/src/resources/User';

async function authenticate(email, password) {
  let [accessToken, userId] = requestToken(email, password);

  library.headers = {
    Authorization: 'Bearer ' + accessToken
  };

  return await User.find(userId);
}

I set library.headers to have an Authorization header with accessToken so all future requests by my ResourceLibrary are authorized.

A later section will cover how to authenticate a user and set the access token using only the User resource class.

The last step of authenticate is a request to User.find(id). This will make a request to /api/v1/users/:id, and the response might look something like:

{
  "data": {
    "type": "users",
    "id": "1",
    "attributes": {
      "email": "[email protected]",
      "user_name": "user1",
      "admin": false
    }
  }
}

The response from authenticate will be an instance of the User class. From here, you can access the various attributes of the authenticated user, if you want to display them somewhere in the application.

let user = authenticate(email, password);

console.log(user.id) // '1'
console.log(user.userName) // user1
console.log(user.email) // [email protected]

console.log(user.attributes()) /*
  {
    email: '[email protected]',
    userName: 'user1',
    admin: false
  }
*/

Each of the attribute names will become camelCased, to fit with the typical standards of JavaScript. You can get each of them directly as properties of the user object, or get all of the attributes by calling user.attributes().

Adding a Resource Index

Before we add more resources that relate to the User class, such as notifications, we should add a file, src/resources/index.js, that will index all of our resources. This has two benefits:

  1. It will clean up our imports by allowing us to destructure src/resources for multiple resources in one import statement instead of using multiple import statements.
  2. It will initialize all of the resources on the ResourceLibrary we will create by calling library.createResource on each, which is necessary for ActiveResource.js to build relationships.
// /src/resources/index.js

import User from './User';

export {
  User
};

Now let’s create a related resource for the User, a Notification. First create a Notification class that belongsTo the User class:

// /src/resources/Notification.js

import library from './library';

class Notification extends library.Base {
  static define() {
    this.belongsTo('user');
  }
}

export default library.createResource(Notification);

Then we add it to the resources index:

// /src/resources/index.js

import Notification from './Notification';
import User from './User';

export {
  Notification,
  User
};

Then, relate the notifications to the User class:

// /src/resources/User.js

class User extends library.Base {
  static define() {
    /* ... */

    this.hasMany('notifications');
  }
}

Now, once we get the user back from authenticate, we can load and display all of its notifications:

let notifications = await user.notifications().load();

console.log(notifications.map(notification => notification.message));

We can also include notifications in our original request for the authenticated user:

async function authenticate(email, password) {
  /* ... */
  
  return await User.includes('notifications').find(userId);
}

This is one of many options available in the DSL.

Reviewing the DSL

Let’s cover what is already possible to request just from the code we’ve written thus far.

You can query a collection of users, or a single user.

let users = await User.all();
let user = await User.first();
user = await User.last();

user = await User.find('1');
user = await User.findBy({ userName: 'user1' });

You can modify the query using chainable relational methods:

// Query and iterate over all users
User.each((user) => console.log(user));

// Include related resources
let users = await User.includes('notifications').all();

// Only respond with user emails as the attributes
users = await User.select('email').all();

// Order users by attribute
users = await User.order({ email: 'desc' }).all();

// Paginate users
let usersPage = await User.page(2).perPage(5).all();

// Filter users by attribute
users = await User.where({ admin: true }).all();

users = await User
  .includes('notifications')
  .select('email', { notifications: ['message', 'createdAt'] })
  .order({ email: 'desc' })
  .where({ admin: false })
  .perPage(10)
  .page(3)
  .all();

let user = await User
  .includes('notification')
  .select('email')
  .first();

Notice that you can compose the query using any amount of chained modifiers, and that you can end the query with .all(), .first(), .last(), or .each().

You can build a user locally, or create one on the server:

let user = User.build(attributes);
user = await User.create(attributes);

Once you have a persisted user, you can send changes to it to be saved on the server:

user.email = '[email protected]';
await user.save();

/* or */

await user.update({ email: '[email protected]' });

You can also delete it from the server:

await user.destroy();

This basic DSL extends to related resources as well, as I’ll demonstrate over the rest of the tutorial. Now we can quickly apply ActiveResource.js to creating the rest of the CMS: posts and comments.

Creating Posts

Create a resource class for Post and associate it with the User class:

// /src/resources/Post.js

import library from './library';

class Post extends library.Base {
  static define() {
    this.belongsTo('user');
  }
}

export default library.createResource(Post);
// /src/resources/User.js

class User extends library.Base {
  static define() {
    /* ... */
    this.hasMany('notifications');
    this.hasMany('posts');
  }
}

Add Post to the resource index as well:

// /src/resources/index.js

import Notification from './Notification';
import Post from './Post';
import User from './User';

export {
  Notification,
  Post,
  User
};

Then tie the Post resource into a form for users to create and edit posts. When the user first visits the form for creating a new post, a Post resource will be built, and each time the form is changed, we apply the change to the Post:

import Post from '/src/resources/Post';

let post = Post.build({ user: authenticatedUser });

onChange = (event) => {
  post.content = event.target.value;
};

Next, add an onSubmit callback to the form to save the post to the server, and handle errors if the save attempt fails:

onSubmit = async () => {
  try {
    await post.save();
    /* successful, redirect to edit post form */
  } catch {
    post.errors().each((field, error) => {
      console.log(field, error.message)
    });
  }
}

Editing Posts

Once the post has been saved, it will be linked to your API as a resource on your server. You can tell if a resource is persisted on the server by calling persisted:

if (post.persisted()) { /* post is on server */ }

For persisted resources, ActiveResource.js supports dirty attributes, in that you can check if any attribute of a resource has been changed from its value on the server.

If you call save() on a persisted resource, it will make a PATCH request containing only the changes made to the resource, instead of submitting the resource’s entire set of attributes and relationships to the server needlessly.

You can add tracked attributes to a resource using the attributes declaration. Let’s track changes to post.content:

// /src/resources/Post.js

class Post extends library.Base {
  static define() {
    this.attributes('content');

    /* ... */
  }
}

Now, with a server-persisted post, we can edit the post, and when the submit button is clicked, save the changes to the server. We can also disable the submit button if no changes have been made yet:

onEdit = (event) => {
  post.content = event.target.value;
}

onSubmit = async () => {
  try {
    await post.save();
  } catch {
    /* display edit errors */
  }
}

disableSubmitButton = () => {
  return !post.changed();
}

There are methods for managing a singular relationship like post.user(), if we wanted to change the user associated with a post:

await post.updateUser(user);

This is equivalent to:

await post.update({ user });

The Comment Resource

Now create a resource class Comment and relate it to Post. Remember our requirement that comments can be in response to a post, or another comment, so the relevant resource for a comment is polymorphic:

// /src/resources/Comment.js

import library from './library';

class Comment extends library.Base {
  static define() {
    this.attributes('content');

    this.belongsTo('resource', { polymorphic: true, inverseOf: 'replies' });
    this.belongsTo('user');

    this.hasMany('replies', {
      as: 'resource',
      className: 'Comment',
      inverseOf: 'resource'
    });
  }
}

export default library.createResource(Comment);

Make sure to add Comment to /src/resources/index.js as well.

We’ll need to add a line to the Post class, too:

// /src/resources/Post.js

class Post extends library.Base {
  static define() {
    /* ... */

    this.hasMany('replies', {
      as: 'resource',
      className: 'Comment',
      inverseOf: 'resource'
    });
  }
}

The inverseOf option passed to the hasMany definition for replies indicates that that relationship is the inverse of the polymorphic belongsTo definition for resource. The inverseOf property of relationships are frequently used when doing operations between relationships. Typically, this property will be automatically determined via class name, but since polymorphic relationships can be one of multiple classes, you must define the inverseOf option yourself in order for polymorphic relationships to have all the same functionality as normal ones.

Managing Comments on Posts

The same DSL that applies to resources also applies to the management of related resources. Now that we’ve set up the relationships between posts and comments, there are a number of ways we can manage this relationship.

You can add a new comment to a post:

onSubmitComment = async (event) => {
  let comment = await post.replies().create({ content: event.target.value, user: user });
}

You can add a reply to a comment:

onSubmitReply = async (event) => {
  let reply = await comment.replies().create({ content: event.target.value, user: user });
}

You can edit a comment:

onEditComment = async (event) => {
  await comment.update({ content: event.target.value });
}

You can remove a comment from a post:

onDeleteComment = async (comment) => {
  await post.replies().delete(comment);
}

Displaying Posts and Comments

The SDK can be used to display a paginated list of posts, and when a post is clicked, the post is loaded on a new page with all of its comments:

import { Post } from '/src/resources';

let postsPage = await Post
  .order({ createdAt: 'desc' })
  .select('content')
  .perPage(10)
  .all();

The above query will retrieve the 10 most recent posts, and to optimize, the only attribute that is loaded is their content.

If a user clicks a button to go to the next page of posts, a change handler will retrieve the next page. Here we also disable the button if there are no next pages.

onClickNextPage = async () => {
  postsPage = await postsPage.nextPage();

  if (!postsPage.hasNextPage()) {
    /* disable next page button */
  }
};

When a link to a post is clicked, we open a new page by loading and displaying the post with all its data, including its comments—known as replies—as well as replies to those replies:

import { Post } from '/src/resources';

onClick = async (postId) => {
  let post = await Post.includes({ replies: 'replies' }).find(postId);

  console.log(post.content, post.createdAt);

  post.replies().target().each(comment => {
    console.log(
      comment.content,
      comment.replies.target().map(reply => reply.content).toArray()
    );
  });
}

Calling .target() on a hasMany relationship like post.replies() will return an ActiveResource.Collection of comments that have been loaded and stored locally.

This distinction is important, because post.replies().target().first() will return the first comment loaded. In contrast, post.replies().first() will return a promise for one comment requested from GET /api/v1/posts/:id/replies.

You could also request the replies for a post separately from the request for the post itself, which allows you to modify your query. You can chain modifiers like order, select, includes, where, perPage, page when querying hasMany relationships just like you can when querying resources themselves.

import { Post } from '/src/resources';

onClick = async (postId) => {
  let post = await Post.find(postId);
  
  let userComments = await post.replies().where({ user: user }).perPage(3).all();
  
  console.log('Your comments:', userComments.map(comment => comment.content).toArray());
}

Modifying Resources After They Are Requested

Sometimes you want to take the data from the server and modify it before using it. For example, you could wrap post.createdAt in a moment() object so that you can display a user-friendly datetime for the user about when the post was created:

// /src/resources/Post.js

import moment from 'moment';

class Post extends library.Base {
  static define() {
    /* ... */

    this.afterRequest(function() {
      this.createdAt = moment(this.createdAt);
    });
  }
}

Immutability

If you work with a state management system that favors immutable objects, all of the behavior in ActiveResource.js can be made immutable by configuring the resource library:

// /src/resources/library.js

import { createResourceLibrary } from 'active-resource';

const library = createResourceLibrary(
  'http://example.com/api/v1',
  {
    immutable: true
  }
);

export default library;

Circling Back: Linking the Authentication System

To wrap up, I’ll show you how to integrate your user authentication system into your User ActiveResource.

Move your token authentication system to the API endpoint /api/v1/tokens. When a user’s email and password are sent to this endpoint, the authenticated user’s data plus the authorization token will be sent in response.

Create a Token resource class that belongs to User:

// /src/resources/Token.js

import library from './library';

class Token extends library.Base {
  static define() {
    this.belongsTo('user');
  }
}

export default library.createResource(Token);

Add Token to /src/resources/index.js.

Then, add a static method authenticate to your User resource class, and relate User to Token:

// /src/resources/User.js

import library from './library';
import Token from './Token';

class User {
  static define() {
    /* ... */

    this.hasOne('token');
  }

  static async authenticate(email, password) {
    let user = this.includes('token').build({ email, password });

    let authUser = await this.interface().post(Token.links().related, user);
    let token = authUser.token();

    library.headers = { Authorization: 'Bearer ' + token.id };

    return authUser;
  }
}

This method uses resourceLibrary.interface(), which in this case is the JSON:API interface, to send a user to /api/v1/tokens. This is valid: An endpoint in JSON:API does not require that the only types posted to and from it are those that it is named after. So the request will be:

{
  "data": {
    "type": "users",
    "attributes": {
      "email": "[email protected]",
      "password": "password"
    }
  }
}

The response will be the authenticated user with the authentication token included:

{
  "data": {
    "type": "users",
    "id": "1",
    "attributes": {
      "email": "[email protected]",
      "user_name": "user1",
      "admin": false
    },
    "relationships": {
      "token": {
        "data": {
          "type": "tokens",
          "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX",
        }
      }
    }
  },
  "included": [{
    "type": "tokens",
    "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX",
    "attributes": {
      "expires_in": 3600
    }
  }]
}

Then we use the token.id to set our library’s Authorization header, and return the user, which is the same as requesting the user via User.find() like we did before.

Now, if you call User.authenticate(email, password), you will receive an authenticated user in response, and all future requests will be authorized with an access token.

ActiveResource.js Enables Rapid JavaScript SDK Development

In this tutorial, we explored the ways that ActiveResource.js can help you quickly build a JavaScript SDK to manage your API resources and their various, sometimes complicated, related resources. You can see all these features and more documented in the README for ActiveResource.js.

I hope you’ve enjoyed the ease in which these operations can be done, and that you’ll use (and maybe even contribute to) my library for your future projects if it fits your needs. In the spirit of open source, PRs are always welcome!

Understanding the basics

What does SDK stand for?

SDK stands for "software development kit," which in a JavaScript context often means a library for interacting with a specific REST API.

What does the acronym ORM stand for?

ORM stands for "object-relational mapping," a technique for working with objects without having to think about where they come from or how they are stored. For example, an ORM library might abstract away an API or the need for SQL knowledge, depending on the layer in which it's used.

What is a CRUD service?

A CRUD service, e.g. a basic REST API, is one that provides an interface for performing "create, read, update, and delete" operations. In other words, it allows basic data storage operations corresponding to the SQL commands INSERT, SELECT, UPDATE, and DELETE and to the HTTP verbs POST, GET, PUT/PATCH, and DELETE.

What does an API do?

An API provides an abstraction of a service, making it easier to work with, usually with the goal of being accessible to a wider group of programmers. For example, Trello's API allows you to programmatically create new Trello cards via HTTP requests.

What is an API key used for?

An API key is a secret token used to formulate requests, allowing the API service provider to positively identify the requester, assuming it remains secret.

What does content management mean?

Content management generally refers to creating and organizing text content to be published on an intranet or on the internet. The most popular content management system (CMS) the past few years running is WordPress.

What is an ORM database?

This is a conflation of terms, but probably refers to an RDBMS, e.g., SQL Server, MySQL, or PostgreSQL. ORM tools sometimes help map objects to SQL back-ends. ActiveResource.js abstracts a different layer, mapping objects to REST API endpoints.