Pup

v1.x

The BasicsMethods

In Meteor, Methods allow us to create a means for communication between the client and server. When a user performs some action on the client, we make a call to a Method on the server. That Method, then, executes code on the server to perform a certain task (e.g., inserting some data into the database).

Scope/Location

In Pup, six Methods are defined for you, each corresponding to a certain server-side task that needs to be performed. Methods live inside of the /imports/api directory in Pup, housed under a directory signifying the scope that they apply to. In Pup, an example Documents collection is defined for you. Methods related to the Documents collection live at /imports/api/Documents/methods.js.

Importing required

Because Pup relies on the /imports directory, Meteor will not automatically load this file—or any file—located within the /imports directory. To ensure that your Methods load properly, you will need to import them into /imports/startup/server/api.js.

If your application has any methods that need to utilize Meteor's latency compensation, make sure to import those Methods into /imports/startup/both/api.js as this file is loaded on both the client and server.

Naming conventions

In addition to file location, to aid in organization Pup uses a simple naming convention for Methods using the scope to which they belong. For example, in the /imports/api/Documents/methods.js, file, all methods are named using the prefix documents.<methodName>. Over on the right, we can see an example of this for the insert method. Because Method names are unique in Meteor, we avoid any collisions and confusion by calling the Method documents.insert. Notice that the function assigned to documents.insert, is given a naming convention, too, using the Method name but in camel case documentsInsert.

Structure

While there are no specific guidelines for how to structure your Methods, it's best to follow a pattern to keep your code consistent and easy to manage. In Pup, Methods are defined following the pattern over on the right.

Once a Method is defined—meaning, we've created the Method function—inside of that function, if we're receiving any values from the client (arguments) we pass those to a Method called check() which helps us to validate the type of data being passed from the client. We do this because any user can call to a Method from the client.

Using the check() method ensures that what we're getting from the client meets our—the developer's—expectations. For example, in the documents.insert method on the right, we expect the doc argument to be an Object with two properties defined on it: title and body, both of a type String.

After we've confirmed that the arguments passed from the client meet our requirements, we use a JavaScript try/catch block to say "try this code and if it fails at any point in time, 'catch' that error message in the exception argument and throw it back to the client as a new Meteor.Error() instance." This ensures that if the code in the try block fails for any reason, an error message will be sent back to the client to notify the user.

Rate limiting

All Methods in Pup are rate-limited for security purposes. Using a custom module located at /imports/modules/rate-limit.js, all Methods specify how many calls are allowed to be made from the client in a given time window. In Pup, all Methods are rate limited to five calls per second. Any calls made beyond this window are blocked and an error is returned to the client making the requests. This prevents your application from experiencing a DDoS (distributed denial of service) attack where an attacker purposefully calls a Method several times in a row in an attempt to overload your server.

/imports/api/Documents/methods.js
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Documents from './Documents';
import rateLimit from '../../modules/rate-limit';

Meteor.methods({
  'documents.insert': function documentsInsert(doc) {
    check(doc, {
      title: String,
      body: String,
    });

    try {
      return Documents.insert({ owner: this.userId, ...doc });
    } catch (exception) {
      throw new Meteor.Error('500', exception);
    }
  },
  [...]
});

rateLimit({
  methods: [
    'documents.insert',
    [...]
  ],
  limit: 5,
  timeRange: 1000,
});

Documents Methods

In Pup, there are three Methods defined for interacting with the Documents collection:

documents.insert is used to create a new document in the Documents collection. By default, it accepts an object doc as an argument, with two properties: title and body, both expected to be String values. The doc value is passed directly to the Documents.insert() method to create the document.

documents.update is used to update an existing document in the Documents collection. By default, it accepts an object doc with three properties: _id (the _id value of the existing document), title, and body. The doc._id value is passed as the first argument to the Documents.update() method to signify which document we want to change in the database, while the second argument specifies how and what we want to change in the database (in this case, we overwrite the existing document with the new values).

documents.remove is used to delete an existing document in the Documents collection. By default, it accepts a string documentId which refers to the document we want to remove from the Documents collection. The documentId value is passed directly to the Documents.remove() method to delete the document.

/imports/api/Documents/methods.js
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Documents from './Documents';
import rateLimit from '../../modules/rate-limit';

Meteor.methods({
  'documents.insert': function documentsInsert(doc) {
    check(doc, {
      title: String,
      body: String,
    });

    try {
      return Documents.insert({ owner: this.userId, ...doc });
    } catch (exception) {
      throw new Meteor.Error('500', exception);
    }
  },
  'documents.update': function documentsUpdate(doc) {
    check(doc, {
      _id: String,
      title: String,
      body: String,
    });

    try {
      const documentId = doc._id;
      Documents.update(documentId, { $set: doc });
      return documentId; // Return _id so we can redirect to document after update.
    } catch (exception) {
      throw new Meteor.Error('500', exception);
    }
  },
  'documents.remove': function documentsRemove(documentId) {
    check(documentId, String);

    try {
      return Documents.remove(documentId);
    } catch (exception) {
      throw new Meteor.Error('500', exception);
    }
  },
});

rateLimit({
  methods: [
    'documents.insert',
    'documents.update',
    'documents.remove',
  ],
  limit: 5,
  timeRange: 1000,
});

OAuth Methods

In Pup, there is one Method defined for managing the OAuth accounts system:

oauth.verifyConfiguration is used to check whether or not an OAuth provider is configured for use with the Meteor accounts system. For each of the services passed from the client, a check is made against the ServiceConfiguration.configurations collection—defined for us by Meteor—to see if configuration exists for that service. This Method is called on load of the <OAuthLoginButtons /> component on both the /login and /signup routes. Note: this method (and the file its located in) is intended to be run on the server-only.

/imports/api/OAuth/server/methods.js
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { ServiceConfiguration } from 'meteor/service-configuration';

Meteor.methods({
  'oauth.verifyConfiguration': function oauthVerifyConfiguration(services) {
    check(services, Array);

    try {
      const verifiedServices = [];
      services.forEach((service) => {
        if (ServiceConfiguration.configurations.findOne({ service })) {
          verifiedServices.push(service);
        }
      });
      return verifiedServices.sort();
    } catch (exception) {
      throw new Meteor.Error('500', exception);
    }
  },
});

User Methods

In Pup, there is one Method defined for interacting with the Meteor.users collection:

users.editProfile is used to help users make changes to their account data in Pup and is called from the /profile route (when a user is logged in). This Method behaves a little bit differently than the other Methods in that it uses an action (outlined below) to perform changes to the users profile across several different functions. Instead of using a try/catch block, when using a module, we rely on a JavaScript Promise which performs a similar process internally.

The editProfile action

Actions are a special name for functions in Pup that are responsible for pulling together several, independent functions into a single, easy to manage process. The reason actions are handy is that they make it easy to run a series of steps all at once, while keeping your code clean. They also make it far easier to come back to code later, making bug fixes and enhancements less cumbersome.

Actions are defined following a simple pattern of exporting a "handler" function wrapped in a JavaScript Promise that calls to other functions in the same file. For each discreet "step" in a process, a function is defined wrapping the code for that step. If a piece of code fails, it can pass its error to the action.reject() method in the handler function (via a JavaScript throw from that code's catch() block), which stops the JavaScript Promise and sends the error back to the client. If an action is successful, it can call to action.resolve() to resolve the JavaScript Promise and send a response back to the client.

Over on the right, we can see this taking place in the editProfile module.

When defining an action, it's generally best to place it in the API scope/directory where it's being used. Alternatively, if an action is used across multiple API scopes, it can be placed in the /imports/modules directory (if intended to be server-only, in /imports/modules/server).

You may be thinking "this is a lot of extra code just to call this function..." Perhaps. But what's lost in extra code is made up for in organization. Assume the process of updating a profile took 10-15 steps instead of just two. You can quickly start to see how this pattern is incredibly helpful for keeping your code neat and organized.

Keep in mind, we've wrapped this code into an action for the sake of example. You're more than welcome to move the contents of the updateUser method here into the method that calls the action in /imports/api/Users/server/methods.js.

/imports/api/Users/server/methods.js
import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import editProfile from './edit-profile';
import rateLimit from '../../../modules/rate-limit';

Meteor.methods({
  'users.editProfile': function usersEditProfile(profile) {
    check(profile, {
      emailAddress: String,
      profile: {
        name: {
          first: String,
          last: String,
        },
      },
    });

    return editProfile({ userId: this.userId, profile })
    .then(response => response)
    .catch((exception) => {
      throw new Meteor.Error('500', exception);
    });
  },
});

rateLimit({
  methods: [
    'users.editProfile',
  ],
  limit: 5,
  timeRange: 1000,
});
/imports/api/Users/server/edit-profile.js
/* eslint-disable consistent-return */

import { Meteor } from 'meteor/meteor';

let action;

const updateUser = (userId, { emailAddress, profile }) => {
  try {
    Meteor.users.update(userId, {
      $set: {
        'emails.0.address': emailAddress,
        profile,
      },
    });
  } catch (exception) {
    throw new Error(`[editProfile.updateUser] ${exception.message}`);
  }
};

const editProfile = ({ userId, profile }, promise) => {
  try {
    action = promise;
    updateUser(userId, profile);
    action.resolve();
  } catch (exception) {
    action.reject(exception.message);
  }
};

export default options =>
  new Promise((resolve, reject) =>
    editProfile(options, { resolve, reject }));

Utility Methods

In Pup, there is one Method defined for handling utility tasks in Pup:

utility.getPage is used to fetch the contents of a static page's Markdown file from /private/pages and parse those contents into HTML for display on the client. This method is called by the <Page /> component when loading a static page.

/imports/api/Utility/server/methods.js
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import getPrivateFile from '../../../modules/server/get-private-file';
import parseMarkdown from '../../../modules/parse-markdown';

Meteor.methods({
  'utility.getPage': function utilityGetPage(fileName) {
    check(fileName, String);
    return parseMarkdown(getPrivateFile(`pages/${fileName}.md`));
  },
});