API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
—
Sails.js uses a flexible action system for handling HTTP requests and business logic. Actions can be traditional controller methods or standalone machine-compatible functions. The actions system provides registration, management, and discovery capabilities with support for input validation, output formatting, and middleware integration.
Register an action with Sails for route binding and execution:
sails.registerAction(action: Function|Dictionary, identity: String, force?: Boolean): voidParameters:
action (Function|Dictionary) - The action function or machine definitionidentity (String) - Unique action identifier (e.g., 'user/find', 'account/login')force (Boolean, optional) - Overwrite existing action if it exists (default: false)Throws:
E_CONFLICT - Action already exists and force is not trueE_INVALID - Action definition is invalidExamples:
// Register a simple function action
sails.registerAction((req, res) => {
return res.json({ message: 'Hello from action!' });
}, 'hello/world');
// Register a machine-compatible action
sails.registerAction({
friendlyName: 'Create user',
description: 'Create a new user account',
inputs: {
email: {
type: 'string',
required: true,
isEmail: true
},
password: {
type: 'string',
required: true,
minLength: 6
},
fullName: {
type: 'string',
required: true
}
},
exits: {
success: {
description: 'User created successfully'
},
emailAlreadyInUse: {
statusCode: 409,
description: 'Email address already in use'
}
},
fn: async function(inputs, exits) {
try {
// Check if email exists
const existingUser = await User.findOne({ email: inputs.email });
if (existingUser) {
return exits.emailAlreadyInUse();
}
// Create new user
const newUser = await User.create({
email: inputs.email,
password: await sails.helpers.passwords.hashPassword(inputs.password),
fullName: inputs.fullName
}).fetch();
return exits.success({
user: newUser,
message: 'User created successfully'
});
} catch (err) {
return exits.serverError(err);
}
}
}, 'user/create');
// Force overwrite existing action
sails.registerAction(newImplementation, 'user/create', true);Retrieve all registered actions:
sails.getActions(): DictionaryReturns: Dictionary - Shallow clone of the internal actions registry
Example:
const actions = sails.getActions();
console.log('Registered actions:');
Object.keys(actions).forEach(identity => {
console.log(`- ${identity}`);
});
// Access specific action
const userCreateAction = actions['user/create'];
if (userCreateAction) {
console.log('User create action found');
}Reload all registered actions from their source files:
sails.reloadActions(cb?: Function): voidParameters:
cb (Function, optional) - Node-style callback (err) => {}Example:
// Reload all actions (useful during development)
sails.reloadActions((err) => {
if (err) {
console.error('Failed to reload actions:', err);
} else {
console.log('Actions reloaded successfully');
}
});
// Promise-style using util.promisify
const { promisify } = require('util');
const reloadActionsAsync = promisify(sails.reloadActions.bind(sails));
try {
await reloadActionsAsync();
console.log('Actions reloaded');
} catch (err) {
console.error('Reload failed:', err);
}Register middleware specifically for actions:
sails.registerActionMiddleware(actionMiddleware: Function|Dictionary, identity: String, force?: Boolean): voidParameters:
actionMiddleware (Function|Dictionary) - Middleware function or definitionidentity (String) - Unique middleware identifierforce (Boolean, optional) - Overwrite existing middlewareExample:
// Register authentication middleware
sails.registerActionMiddleware(
(req, res, proceed) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
return proceed();
},
'auth/require-login'
);
// Register validation middleware
sails.registerActionMiddleware({
friendlyName: 'Validate API key',
fn: function(req, res, proceed) {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid API key' });
}
return proceed();
}
}, 'auth/validate-api-key');Simple function-based actions for basic request handling:
// Basic function action
function simpleAction(req, res) {
const { id } = req.params;
return res.json({
message: 'Simple action response',
id: id
});
}
sails.registerAction(simpleAction, 'simple/action');
// Async function action
async function asyncAction(req, res) {
try {
const data = await fetchDataFromDatabase();
return res.json(data);
} catch (err) {
return res.serverError(err);
}
}
sails.registerAction(asyncAction, 'async/action');Machine-compatible actions with input validation and structured exits:
const machineAction = {
friendlyName: 'Find users',
description: 'Find users with optional filtering',
inputs: {
limit: {
type: 'number',
defaultsTo: 10,
min: 1,
max: 100
},
skip: {
type: 'number',
defaultsTo: 0,
min: 0
},
search: {
type: 'string',
description: 'Search term for user names or emails'
}
},
exits: {
success: {
description: 'Users found successfully',
outputType: {
users: ['ref'],
totalCount: 'number'
}
}
},
fn: async function(inputs, exits) {
let criteria = {};
if (inputs.search) {
criteria.or = [
{ fullName: { contains: inputs.search } },
{ email: { contains: inputs.search } }
];
}
const users = await User.find(criteria)
.limit(inputs.limit)
.skip(inputs.skip);
const totalCount = await User.count(criteria);
return exits.success({
users: users,
totalCount: totalCount
});
}
};
sails.registerAction(machineAction, 'user/find');Find the first explicit route that matches a given action target:
sails.getRouteFor(routeQuery: String|Dictionary): {url: String, method: String}Parameters:
routeQuery (String|Dictionary) - Target action or route queryReturns: Object with url and method properties
Throws:
E_NOT_FOUND - No matching route foundE_USAGE - Invalid route queryExamples:
// Find route by controller method
try {
const route = sails.getRouteFor('UserController.find');
console.log(`Route: ${route.method.toUpperCase()} ${route.url}`);
// Output: Route: GET /api/users
} catch (err) {
if (err.code === 'E_NOT_FOUND') {
console.log('Route not found');
}
}
// Find route by action identity
const loginRoute = sails.getRouteFor('entrance/login');
console.log(loginRoute); // { url: '/login', method: 'get' }
// Find route by object query
const userShowRoute = sails.getRouteFor({
target: 'UserController.findOne'
});
console.log(userShowRoute); // { url: '/api/users/:id', method: 'get' }Get URL pattern for a route target:
sails.getUrlFor(routeQuery: String|Dictionary): StringParameters:
routeQuery (String|Dictionary) - Target action or route queryReturns: String - URL pattern for the route
Examples:
// Get URL pattern for action
const userCreateUrl = sails.getUrlFor('user/create');
console.log(userCreateUrl); // '/api/users'
// Get URL with parameters
const userShowUrl = sails.getUrlFor('UserController.findOne');
console.log(userShowUrl); // '/api/users/:id'
// Build actual URL with parameters
const actualUrl = userShowUrl.replace(':id', '123');
console.log(actualUrl); // '/api/users/123'Create virtual requests for testing actions without HTTP server:
sails.request(opts: {url: String, method?: String, params?: Dictionary, headers?: Dictionary}, cb?: Function): MockClientResponse
sails.request(address: String, params?: Dictionary, cb?: Function): MockClientResponseParameters:
opts (Dictionary) - Request configuration object
url (String) - Target URL/routemethod (String, optional) - HTTP method (default: 'GET')params (Dictionary, optional) - Request parameters/bodyheaders (Dictionary, optional) - Request headersaddress (String) - Simple URL/route addressparams (Dictionary, optional) - Request parameterscb (Function, optional) - Response callbackReturns: MockClientResponse - Stream-like response object
Examples:
// Basic virtual request
sails.request('/api/users', (err, res, body) => {
if (err) throw err;
console.log('Response status:', res.statusCode);
console.log('Response body:', body);
});
// Request with parameters
sails.request({
url: '/api/users',
method: 'POST',
params: {
email: 'test@example.com',
fullName: 'Test User',
password: 'password123'
}
}, (err, res, body) => {
console.log('User created:', body);
});
// Request with headers
sails.request({
url: '/api/protected',
headers: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/json'
}
}, (err, res, body) => {
console.log('Protected resource:', body);
});
// Promise-style virtual request
const response = await new Promise((resolve, reject) => {
sails.request('/api/users/123', (err, res, body) => {
if (err) return reject(err);
resolve({ res, body });
});
});Actions are typically located in:
api/controllers/
├── UserController.js
├── AuthController.js
└── AdminController.jsapi/actions/
├── user/
│ ├── create.js
│ ├── find.js
│ └── update.js
├── auth/
│ ├── login.js
│ └── logout.js
└── admin/
└── dashboard.jsController-based action (api/controllers/UserController.js):
module.exports = {
find: async function(req, res) {
const users = await User.find();
return res.json(users);
},
create: async function(req, res) {
const newUser = await User.create(req.allParams()).fetch();
return res.json(newUser);
}
};Standalone action (api/actions/user/create.js):
module.exports = {
friendlyName: 'Create user',
description: 'Create a new user account',
inputs: {
email: { type: 'string', required: true },
fullName: { type: 'string', required: true }
},
fn: async function(inputs) {
const user = await User.create(inputs).fetch();
return { user };
}
};Actions receive different parameters based on their format:
function actionHandler(req, res) {
// req - HTTP request object
// res - HTTP response object
// Direct access to Sails globals (User, sails, etc.)
}{
fn: async function(inputs, exits) {
// inputs - Validated input parameters
// exits - Exit functions (success, error, etc.)
// Access to this.req and this.res for HTTP context
}
}Actions support comprehensive error handling:
// Function action error handling
sails.registerAction(async (req, res) => {
try {
const result = await riskyOperation();
return res.json(result);
} catch (err) {
if (err.code === 'E_NOT_FOUND') {
return res.notFound();
}
return res.serverError(err);
}
}, 'risky/operation');
// Machine action error handling
sails.registerAction({
fn: async function(inputs, exits) {
try {
const result = await performOperation(inputs);
return exits.success(result);
} catch (err) {
if (err.name === 'ValidationError') {
return exits.invalid(err.details);
}
throw err; // Will be caught by framework
}
},
exits: {
invalid: {
statusCode: 400,
description: 'Invalid input provided'
}
}
}, 'validated/operation');Actions can be reloaded during development:
// Watch for file changes and reload actions
if (sails.config.environment === 'development') {
const chokidar = require('chokidar');
chokidar.watch('api/actions/**/*.js').on('change', () => {
sails.reloadActions((err) => {
if (!err) console.log('Actions reloaded');
});
});
}// Test action directly
describe('User Actions', () => {
it('should create user', (done) => {
sails.request({
url: '/api/users',
method: 'POST',
params: {
email: 'test@example.com',
fullName: 'Test User'
}
}, (err, res, body) => {
expect(res.statusCode).to.equal(201);
expect(body.user).to.exist;
done();
});
});
});The Sails actions system provides a flexible foundation for building scalable web applications with comprehensive input validation, error handling, and testing capabilities.
Install with Tessl CLI
npx tessl i tessl/npm-sails