Machine as Action Controller
machine-as-action is a Node.js utility designed to bridge the gap between 'machines' – a pattern for encapsulating reusable business logic – and standard HTTP/WebSocket request-response cycles. It allows developers to define a machine and then wrap it with `asAction` to automatically handle incoming request parameters as machine inputs and map machine exits to various HTTP response types, including JSON data, rendered views, or redirects. The current stable version is 10.3.1, though recent release notes show significant changes around version 7.x. While there isn't a strict, rapid release cadence apparent from the provided data, updates have been made. Its primary differentiator lies in its deep integration with the 'machine' pattern, providing a structured and convention-over-configuration approach to expose API endpoints, particularly useful in frameworks like Sails.js where machines are a core architectural component. It offers granular control over response status codes and view rendering based on the outcome of a machine's execution, aiming to reduce boilerplate in controller logic.
Common errors
-
Error: E_DOUBLE_WRAP: This action has already been wrapped!
cause Attempting to wrap a machine with `asAction` more than once, or passing an already-wrapped action to `asAction`.fixEnsure `asAction` is called only once for each machine. Store the result of `asAction` and reuse it, rather than calling `asAction` multiple times with the same input. -
Error: E_INVALID_MACHINE: The provided machine definition is invalid.
cause The object passed to `asAction` does not conform to the expected machine specification (missing `fn`, malformed `inputs`/`exits`, etc.).fixVerify that the machine definition object has a `fn` property, and correctly structured `inputs` and `exits` objects as per the machine spec. -
ReferenceError: require is not defined in ES module scope
cause Attempting to use `require('machine-as-action')` in an ECMAScript module (ESM) environment without proper transpilation or dynamic import.fixIf working in a pure ESM project, you might need to use dynamic `import('machine-as-action')` or configure your build system (e.g., Webpack, Rollup) to handle CommonJS modules within an ESM context. This package is primarily CJS. -
TypeError: Cannot read properties of undefined (reading 'fn')
cause The object passed to `asAction` is `undefined` or `null`, or does not have the expected `fn` property, indicating an invalid or missing machine definition.fixDouble-check that the variable holding your machine definition is correctly imported or defined and is a valid object containing a `fn` function.
Warnings
- breaking As of v6.1.3, undocumented and experimental tolerance of 'loose functions' (functions not conforming to the machine spec) as machine definitions is no longer supported.
- breaking Version 7.0.11 introduced significant changes to custom response types and error handling, potentially altering the default behavior or configuration for `responseType`, `statusCode`, and `viewTemplatePath` in machine exits. Existing custom response logic might require adjustments.
- gotcha When generating stub data and not in production mode, `machine-as-action` automatically attaches an `X-Stub` header to responses. This behavior was introduced in v6.1.0.
- gotcha For non-success exits that do not explicitly configure a `responseType` (e.g., 'view', 'redirect'), `machine-as-action` defaults to using a 500 status code.
Install
-
npm install machine-as-action -
yarn add machine-as-action -
pnpm add machine-as-action
Imports
- asAction
import asAction from 'machine-as-action';
const asAction = require('machine-as-action');
Quickstart
const asAction = require('machine-as-action');
// Simulate Express/Sails req/res objects for demonstration
const mockReq = (query = {}, body = {}) => ({ query, body });
const mockRes = () => {
let _status = 200;
let _data = null;
let _view = null;
let _redirect = null;
return {
status: function(code) { _status = code; return this; },
json: function(data) { _data = data; console.log(`[Response JSON] Status: ${_status}, Data: ${JSON.stringify(_data)}`); },
send: function(data) { _data = data; console.log(`[Response Send] Status: ${_status}, Data: ${data}`); },
view: function(template, locals) { _view = { template, locals }; console.log(`[Response View] Status: ${_status}, Template: ${_view.template}, Locals: ${JSON.stringify(_view.locals)}`); },
redirect: function(url) { _redirect = url; console.log(`[Response Redirect] Status: ${_status}, URL: ${_redirect}`); },
serverError: function(err) { _status = 500; _data = { error: err.message }; console.error(`[Server Error] Status: ${_status}, Data: ${JSON.stringify(_data)}`); }
};
};
// 1. Define an inline machine
const greetMachine = {
inputs: {
name: { type: 'string', required: true, example: 'World' }
},
exits: {
success: { outputExample: 'Hello World!' },
error: { outputExample: 'Could not greet.' }
},
fn: function(inputs, exits) {
if (inputs.name) {
return exits.success(`Hello, ${inputs.name}!`);
}
return exits.error(new Error('Name input is missing.'));
}
};
// 2. Wrap the machine as an action
const greetAction = asAction(greetMachine);
// 3. Simulate an HTTP request for greetAction
console.log('--- Simulating a simple custom action ---');
greetAction(mockReq({ name: 'Registry User' }), mockRes());
// 4. Define an action with a custom responseType (e.g., 'view')
const showProfilePageAction = asAction({
inputs: {
userId: { type: 'string', required: true, example: 'user-123' }
},
exits: {
success: {
responseType: 'view',
viewTemplatePath: 'profile/show',
outputExample: { username: 'Alice', id: 'user-123' }
}
},
fn: function(inputs, exits) {
// In a real app, this would fetch user data from a database
const userData = { username: `User ${inputs.userId}`, id: inputs.userId };
return exits.success(userData);
}
});
// 5. Simulate an HTTP request for the view action
console.log('\n--- Simulating an action rendering a view ---');
showProfilePageAction(mockReq({ userId: '456' }), mockRes());