As of v1.0, all Sails apps come with built-in support for helpers, simple utilities that let you share Node.js code in more than one place. This helps you avoid repeating yourself, and makes development more efficient by reducing bugs and minimizing rewrites. Like actions2, this also makes it much easier to create documentation for your app.
In Sails, helpers are the recommended approach for pulling repeated code into a separate file, then reusing that code in various actions, custom responses, command-line scripts, unit tests, or even other helpers. You don't have to use helpers—in fact you might not even need them right away. But as your code base grows, helpers will become more and more important for your app's maintainability (plus, they're really convenient).
For example, in the course of creating the actions that your Node.js/Sails app uses to respond to client requests, you will sometimes find yourself repeating code in several places. That can be pretty bug-prone, of course, not to mention annoying. Fortunately, there's a neat solution: replace the duplicate code with a call to a custom helper:
const greeting = await sails.helpers.formatWelcomeMessage('Bubba');
sails.log(greeting);
// => "Hello, Bubba!"
Helpers can be called from almost anywhere in your code, as long as that place has access to the
sails
app instance.
Here's an example of a simple, well-defined helper:
// api/helpers/format-welcome-message.js
module.exports = {
friendlyName: 'Format welcome message',
description: 'Return a personalized greeting based on the provided name.',
inputs: {
name: {
type: 'string',
example: 'Ami',
description: 'The name of the person to greet.',
required: true
}
},
fn: async function (inputs, exits) {
const result = `Hello, ${inputs.name}!`;
return exits.success(result);
}
};
Though simple, this file displays several characteristics of a good helper: it starts with a friendly name and description that make it immediately clear what the utility does, it describes its inputs so that it’s easy to see how the utility is used, and it accomplishes a discrete task in the simplest way possible.
Look familiar? Helpers follow the same specification as shell scripts and actions2.
fn
functionThe core of the helper is the fn
function, which contains the actual code that the helper will run. The function takes two arguments: inputs
(a dictionary of input values, or "argins") and exits
(a dictionary of callback functions). The job of fn
is to utilize and process the argins, and then trigger one of the provided exits to return control back to whatever code called the helper. Note that, as opposed to a typical JavaScript function that uses return
to provide output to the caller, helpers provide that result value by passing it in to exits.success()
.
A helper’s declared inputs are analogous to the parameters of a typical JavaScript function: they define the values that the code has to work with. However, unlike standard JavaScript function parameters, inputs are validated automatically. If a helper is called using argins of the wrong type for their corresponding inputs or missing a value for a required input, it will trigger an error. Thus, helpers are self-validating.
Input for a helper are defined in the inputs
dictionary. Each input definition is composed of, at minimum, a type
property. Helper inputs support types like:
string
- a string valuenumber
- a number value (both integers and floats are valid)boolean
- the value true
or false
ref
- a JavaScript variable reference (can be any value, including dictionaries, arrays, functions, streams, etc.)These are the same data types (and related semantics) that you might already be accustomed to from defining model attributes.
So, as you might expect, you can provide a default value for an input by setting its defaultsTo
property. Or you can make it required by setting required: true
. You can even use allowNull
and almost any of the higher-level validation rules like isEmail
.
The arguments you pass in when calling a helper correspond with the order of keys in that helper's declared inputs
. Alternatively, if you'd rather pass in argins by name, use .with()
:
const greeting = await sails.helpers.formatWelcomeMessage.with({ name: 'Bubba' });
Exits describe all the different possible outcomes a helper can have, good or bad. Every helper automatically supports the error
and success
exits.
When calling a helper, if its fn
triggers success
, then it will return normally. But if its fn
triggers some exit other than success
, then it will throw an Error (unless .tolerate()
was used).
When necessary, you can also expose other custom exits (known as "exceptions"), allowing the userland code that calls your helper to handle specific, exceptional cases. This helps guarantee your code’s transparency and maintainability by making it painless and easy to declare and negotiate errors.
Exceptions (custom exits) for a helper are defined in the
exits
dictionary. It is a good practice to provide all custom exceptions with an explicitdescription
property.
Imagine a helper called “inviteNewUser” which exposes a custom emailAddressInUse
exit. The helper's fn
might trigger this custom exit if the provided email already exists, allowing your userland code to handle this specific scenario-- without muddying up your result values or resorting to extra try/catch
blocks.
For example, if this helper was called from within an action that has its own "badRequest" exit:
const newUserId = sails.helpers.inviteNewUser('bubba@hawtmail.com')
.intercept('emailAddressInUse', 'badRequest');
The fancy-looking shorthand above is just a quicker way to write:
.intercept('emailAddressInUse', (err)=>{ return 'badRequest'; });
As for .intercept(), it's just another shortcut so you're not forced to write custom try/catch blocks to negotiate these errors by hand all the time.
Internally, your helper's fn
is responsible for triggering one of its exits, either by throwing a special exit signal or by invoking an exit callback (e.g. exits.success('foo')
). If your helper sends back a result through the success exit (e.g. 'foo'
), then that will be the return value of the helper.
Note: For non-success exits, Sails will use the exit's predefined description to create an appropriate JavaScript Error instance automatically, if needed.
By default, all helpers are considered asynchronous. While this is a safe default assumption, it's not always true. When you know for certain that your helper is synchronous, you can optimize performance by telling Sails using the sync: true
property. This allows userland code to call the helper without await
. But if you set sync
to true
, don't forget to change fn: async function
to fn: function
!
Note: Calling an asynchronous helper without
await
will not work.
req
in a helperIf you’re designing a helper that parses request headers specifically for use from within actions, then you'll want to take advantage of pre-existing methods and/or properties of the request object. The simplest way to allow the code in your action to pass along req
to your helper is to define a type: 'ref'
input:
inputs: {
req: {
type: 'ref',
description: 'The current incoming request (req).',
required: true
}
}
Then, to use your helper in your actions, you might write code like this:
const headers = await sails.helpers.parseMyHeaders(req);
Sails provides a built-in generator that you can use to create a new helper automatically:
sails generate helper foo-bar
This will create a file api/helpers/foo-bar.js
that can be accessed in your code as sails.helpers.fooBar
. The file that is initially created will be a generic helper with no inputs and just the default exits (success
and error
), which immediately triggers its success
exit when executed.
Whenever a Sails app loads, it finds all of the files in api/helpers/
, compiles them into functions, and stores them in the sails.helpers
dictionary using the camel-cased version of the filename. Any helper can then be invoked from your code, simply by calling it with await
, and providing some argin values:
const result = await sails.helpers.formatWelcomeMessage('Dolly');
sails.log('Ok it worked! The result is:', result);
This is roughly the same usage you might already be familiar with from model methods like
.create()
.
If a helper declares the sync
property, you can also call it without await
:
const greeting = sails.helpers.formatWelcomeMessage('Timothy');
But before you remove await
, make sure the helper is actually synchronous. Without await
an asynchronous helper will never execute!
If your application uses many helpers, you might find it helpful to group related helpers into subdirectories. For example, imagine you had a number of user
helpers and several item
helpers, organized in the following directory structure
api/
helpers/
user/
find-by-username.js
toggle-admin-role.js
validate-username.js
item/
set-price.js
apply-coupon.js
When calling these helpers, each subfolder name (e.g. user
and item
) becomes an additional property layer in the sails.helpers
object, so you can call find-by-username.js
using sails.helpers.user.findByUsername()
and you can call set-price.js
with sails.helpers.item.setPrice()
.
For more information, you can read a conversation between Ryan Emberling and Mike McNeil which goes into more detail about this use case, including some general tips and tricks for working with custom helpers and organics.
For more granular error handling (and even for those exceptional cases that aren't quite errors) you may be used to setting some kind of error code, then sniffing out the error. This approach works fine, but it can be time-consuming and hard to track.
Fortunately, there are a few different ways to conveniently handle errors in Sails helpers. See the pages on .tolerate(), .intercept(), and special exit signals for more information.
While the usage in this example is excessive, it's easy to imagine a scenario in which it would be helpful to rely on custom exits like notUnique
. Still, you don't want to have to handle every custom exit every time. Ideally, you'd only have to handle a custom exit in your userland code when necessary: whether to implement a feature of some kind or even to improve user experience or provide a better internal error message.
Luckily, Sails helpers support "automatic exit forwarding". That means userland code can choose to integrate with as few or as many custom exits as you like, on a case by case basis. In other words, when calling a helper it's OK to completely ignore its custom notUnique
exit if you don't need it. That way, your code remains as concise and intuitive as possible. And if things change, you can always revise your code to handle the custom exit later.
sails-hook-organics
(which is bundled in the "Web App" template) comes with several free, open-source, and MIT-licensed helpers for many common use cases. Have a look!