Skip to content

JavaScript Code Formatting and Style

Language: JavaScript

The complete style guide for JavaScript code in Superloom modules. ESLint enforces most of these rules automatically; the rest are conventions every contributor and AI agent is expected to follow. The compressed mirror of this guide lives in AGENTS.md.

On This Page


Tooling

ToolRoleCommand
ESLint v10+Lint and auto-fixnpm run lint, npm run lint:fix
Flat configRequired by ESLint v10Every module ships an eslint.config.js
Editor integrationAuto-fix on saveSee docs/guide/ide-setup.md

ESLint catches no-var, prefer-const, no-unused-vars, no-useless-assignment, and the formatting rules below. CI runs npm run lint on every push - fix locally before pushing.

General Principles

  • Keep it simple. Code should be easy to read and understand.
  • Functions over classes. Prefer plain functions and object literals.
  • Node.js standards. Follow standard Node.js conventions; no clever tricks.

Source Style

RuleExample
Single quotes for strings'hello' not "hello"
2-space indentationSpaces, never tabs
No trailing commas[1, 2, 3] not [1, 2, 3,]
No trailing whitespaceEditor must trim
Newline at end of fileRequired
SemicolonsRequired at end of every statement
Space before function parenfunction (param) not function(param)
Space before blockif (cond) { not if (cond){
Space around operatorsa + b not a+b, x === y not x===y
Object brace spacing{ key: value } not {key:value}
Array bracket spacing[1, 2, 3] not [ 1, 2, 3 ]
Comma spacing[1, 2, 3] not [1 ,2 ,3]

Additional Formatting Rules

All modules use "type": "module" in package.json. The rules below apply to every module.

Rules:

  • The 3/2/1 vertical spacing rule
  • Section banners (Module-Loader START/END, createInterface START/END, Public Functions START/END)
  • JSDoc blocks and step comments
  • The Info header
  • Private _Name object enclosure
  • All naming conventions

Component factory files in an ESM module use export default function (Lib, CONFIG, ERRORS, Parts, Registry, Style) { ... } with the same internal structure as any factory: Static Constants START/END, Public Functions START/END, and Private Functions START/END sections separated by /// banners. The return Name; at the factory bottom and the }/////////////////////////// Component Factory END ///////////////////////////// closing banner are unchanged.


Vertical Spacing (3/2/1 Rule)

The vertical spacing follows a strict hierarchy that creates visual structure at three scales:

SpacingPurpose
3 blank linesBetween major module sections (Module-Loader, Module Exports, Public Functions, Private Functions, createInterface)
2 blank linesBetween individual function definitions
1 blank lineAfter opening {, before closing }, between logical blocks inside a function

Spacing Reference Table

LocationBlank linesWhy
Between let declarations at module top1let Lib; → blank → let CONFIG;
After CONFIG, before Module-Loader header2Marks the start of the loader section
Between major module sections3Largest visual separator
Between function definitions2Functions are visually distinct units
After function opening {1Internal breathing room
Before function closing }1Internal breathing room
Between logical blocks inside a function1Groups related statements
Before comment headers (// Initialize, // Run query)1Sets the comment apart visually
After JSDoc, before function body1Separates docs from code
Between if/else blocks1Visual branch separation
After return statement1Returns are visually isolated

Standard Module Skeleton

javascript
// Info: [Module purpose - 1 line]
// [What it does - 1 line]
// [Pattern indicator - 1 line]


// Shared dependencies (injected by loader; avoids passing Lib everywhere)
let Lib;

// Domain config (injected; constants/enums, not runtime env)
let CONFIG;


/////////////////////////// Module-Loader START ////////////////////////////////
export default function loader (shared_libs, config) {

  Lib = shared_libs;
  CONFIG = config;

  return ModuleName;

};//////////////////////////// Module-Loader END /////////////////////////////////



////////////////////////////Public Functions START//////////////////////////////
const ModuleName = {

  /********************************************************************
  Function description.

  @param {Type} name - Description
  @return {Type} - Description
  *********************************************************************/
  functionName: function (params) {

    // Compute the result
    const result = doSomething(params);

    // Return the computed value
    return result;

  }

};////////////////////////////Public Functions END///////////////////////////////

In helper modules the loader and the export are one merged function under the Module-Loader banner - a named export default function loader (...) - with no separate export section; splitting them adds a banner and an indirection without adding information. Application modules (entity controllers, services, models) keep the separate export section shown in entity-creation-guide-js.md, because their loader wires Lib/CONFIG/ERRORS while the export returns a separately-declared module object.


Section Header Hierarchy

Three levels of section separators signal different granularity. Use them from coarsest to finest.

LevelMarkerPurpose
1/////////////////////////// [Name] START /////////////////////Major module sections: Module-Loader, createInterface, Public Functions, Private Functions (+ Module Exports in application modules)
2// ~~~~~~~~~~~~~~~~~~~~ [Name] ~~~~~~~~~~~~~~~~~~~~ + one-line purposeSubsections inside a public/private function object, grouped by responsibility
3// [comment]Inline comment above a logical block inside a function

When to Use Level 2 Subsections

Use Level 2 subsections inside public or private function objects when either:

  • The module has 5+ exported functions, or
  • The functions fall into 2+ clear responsibility groups (e.g., Core Execution, Read Helpers, Transactions, Lifecycle)

Level 2 Example

javascript
const ModuleName = {

  // ~~~~~~~~~~~~~~~~~~~~ Read Helpers ~~~~~~~~~~~~~~~~~~~~
  // Functions that return data without modifying state.

  /********************************************************************
  Function description
  *********************************************************************/
  getRow: function () { /* ... */ },


  /********************************************************************
  Function description
  *********************************************************************/
  getRows: function () { /* ... */ },


  // ~~~~~~~~~~~~~~~~~~~~ Write Helpers ~~~~~~~~~~~~~~~~~~~~
  // Functions that modify state.

  /********************************************************************
  Function description
  *********************************************************************/
  insert: function () { /* ... */ }

};

Subsection rules:

  • Subsection name is a short noun phrase, two to four words, title-cased
  • The purpose comment under the marker explains what binds the functions together. Keep it concise - one line is ideal, up to 4 lines is acceptable when the grouping needs real motivation (dialect quirks, hot-path notes, security invariants)
  • Leave 2 blank lines between the last function of one subsection and the next subsection marker

Private Functions Enclosure

Private helpers inside createInterface must always be declared as a const _Name = { ... } object literal. Never use bare Name.method = function(...) property assignments on the public object.

javascript
///////////////////////////Private Functions START/////////////////////////////
const _Validators = {

  assertNonEmptyString: function (value, field, fn_name) {
    // ...
  },

  assertEnum: function (value, field, fn_name, allowed) {
    // ...
  }

};///////////////////////////Private Functions END//////////////////////////////

Rules:

  • The private enclosure is named _Name where Name matches the public object (e.g., _Auth, _Validators, _Cookie, _RecordShape)
  • All call sites inside the public object use _Name.method(...), not Name.method(...)
  • The enclosure follows immediately after the ///Private Functions START/// banner
  • If a private helper calls another private helper, it uses _Name.otherHelper(...) as well

Section Closing Banners

The closing }; of every named section must be combined on the same line as the ///...END.../// banner. Never place it on a separate line.

The closing }; combined with the END banner:

javascript
  };///////////////////////////Public Functions END////////////////////////////////

  };///////////////////////////Private Functions END//////////////////////////////

};/////////////////////////// createInterface END ////////////////////////////////

This applies to every section closer: Public Functions END, Private Functions END, createInterface END, Module-Loader END, Module Exports END.


Variable Declarations

DeclarationWhen to use
constDefault. Use for any variable whose binding never changes - including objects and arrays (the reference does not change, only the contents)
letOnly when the binding is reassigned. Examples: let Lib; reassigned in loader; let count = 0; in a loop
varNever. All modern Node.js (>= 14) and browsers support block-scoped let/const. var's function scoping and hoisting cause subtle bugs

ESLint enforces this via no-var (error) and prefer-const (error).

Variable Initialization

Do not initialize a variable with a placeholder value if it will be reassigned before it is ever read. ESLint v10's no-useless-assignment rule flags this.

PatternVerdict
let result; then result = calculate();✅ Initializer omitted, assigned before read
let result = ''; then result = calculate();❌ Empty string is never read - useless assignment
let result = ''; then conditional reassignment in some branches✅ Useful when some branches read without reassigning

Naming Conventions

Function Naming

Function verbs, return shapes, confusable pairs, and banned verbs are settled in function-naming.md. That document is the single source of truth for which verb to use and what each verb returns.

The Multi-HTTP-Method Pattern is the one function-naming rule that lives here, because it is a suffix convention rather than a verb choice:

  • Standard pattern: descriptive verb-noun, no HTTP method suffix - createUser(), deleteFile(), sendEmail()
  • Multi-HTTP-method pattern: when 2+ functions do the same thing with different HTTP methods, suffix with the method:
    • generateUploadUrlPut() - PUT method (simple URL)
    • generateUploadUrlPost() - POST method (with form fields)
    • generateDownloadUrlGet() - GET method
  • Decision rule: apply HTTP method suffixes only when 2+ functions exist for the same goal. Single-method functions stay plain
  • Avoid generic wrappers: prefer generateUploadUrlPut() and generateDownloadUrlGet() over a generateUrls() convenience function

Module Naming

Use category-based naming so related modules sort and group together:

CategoryPrefixExamples
Relational databasessql-js-server-helper-sql-mysql, js-server-helper-sql-postgres, js-server-helper-sql-sqlite
NoSQL databasesnosql-js-server-helper-nosql-mongodb
AWS NoSQLnosql-aws-js-server-helper-nosql-aws-dynamodb
AWS object storagestorage-aws-js-server-helper-storage-aws-s3, js-server-helper-storage-aws-s3-url-signer
AWS message queuequeue-aws-js-server-helper-queue-aws-sqs

Vendor placement:

  • Vendor name as infix for cloud-specific services (-aws-, -gcp-)
  • No vendor prefix for vendor-agnostic modules (sql-mysql, nosql-mongodb)
  • Pattern: [category]-[vendor]-[service] for cloud-specific modules

Module Terminology (Consistent Across Modules)

TermMeaning
LibShared library container injected at loader time
CONFIGEntity-specific or module-specific configuration
loaderDependency injection function
shared_libsParameter name for the Lib argument in loader
config_moduleParameter name for module configuration in loader

Parameter Naming

  • No underscore prefix on parameters. Use an inline // eslint-disable-line no-unused-vars comment on the function signature instead of _param to suppress ESLint's no-unused-vars warning. This rule is now mechanically enforced by the shared config's args: 'after-used' setting, which ignores unused params that precede a used one
  • No void param; statements. void executes at runtime (as a no-op expression) and is a non-standard workaround. Always use the eslint-disable-line approach instead

Shared ESLint Configuration

@superloomdev/js-helper-eslint-config is the single source of truth for all lint rules across every Superloom module. Every module's eslint.config.js is a three-line re-export of one of its presets (base, browser, app). Per-module rule overrides are not permitted. If a rule value needs to change, it changes in the shared config package and propagates to all modules on the next install.

The shared config defines four presets:

PresetUse caseKey differences from base
baseNode.js ESM modules (all helper modules)Node 24 globals, sourceType: 'module'
esmAlias of base (retained for backward compatibility)Identical to base
browserWeb-facing code that uses browser APIsAdds browser globals (document, window, etc.)
appApplication-tier repos with JSX and browser globalsLayers on browser + JSX parsing, varsIgnorePattern: '^React$'

Clean parameter name with an inline directive when needed:

javascript
const createInterface = function (Lib, CONFIG, ERRORS) { // eslint-disable-line no-unused-vars

Uniform Factory Signatures

When a module family (e.g. parts/) uses a uniform factory signature for consistency so the parent can call all parts identically, some parts will not consume every parameter. This is expected and correct. Do not change the signature to match only what is consumed today.

The signature is uniform across all parts: (Lib, CONFIG, ERRORS). When a part only uses Lib today, CONFIG and ERRORS are suppressed with eslint-disable-line. The directive is optional. Add it only when there is an unused parameter; remove it when all parameters are consumed.

javascript
const createInterface = function (Lib, CONFIG, ERRORS) { // eslint-disable-line no-unused-vars

Never use void CONFIG; or void ERRORS; as a workaround for this case.

Public Data Field Naming

Every field in a public return shape, and every key in a public options/params object, is snake_case - regardless of what the underlying driver or SDK calls it.

  • Vendor wrappers normalize driver fields at the boundary: mysql's affectedRows becomes affected_rows, S3 metadata becomes content_type, SQS returns message_id.
  • Input parameter keys follow the same rule (keys_by_table, not keysByTable).
  • Leaking driver camelCase (matchedCount, deletedCount) couples consumers to the vendor's naming and breaks cross-backend uniformity - the same reason vendor wording is banned from error strings.

Function Parameter Conventions

Use this decision rule for every function signature:

SituationPatternName
4+ parameters, or any optional parameter, or parameters likely to growSingle options objectoptions
3 or fewer parameters, all required, all unlikely to changePositional paramsparam1, param2, param3

Never use args as a parameter name. Use options for named-property bundles and plain descriptive names for positional params.

7+ fields, use an options object:

javascript
applyLimits: function (options) {
  options.existing; options.limits.total_max; // etc.
}

2 required positional parameters:

javascript
composeCookieName: function (cookie_prefix, tenant_id) { }

Closed-over dependencies are never in the options object. If a private helper function accesses Lib, CONFIG, or store from the enclosing closure, do not pass those as fields in the options bundle. Only pass the per-call data the function cannot otherwise reach.

Only pass per-call data that the function cannot reach from its enclosing closure:

javascript
_Auth.scheduleBackgroundRefresh(instance, record, ttl_seconds, tenant_id);

Multi-line Literals

Multi-line layouts keep git diff readable when fields are added later.

ConstructAlways multi-lineSingle-line acceptable
Return objectsYes - always multi-lineNever
package.jsonYes - always multi-lineNever
YAML arraysYes - branches:\n - mainNever branches: [main]
Nested JSONYes when 2+ items or nested structureSingle-line OK only for {} and []
JS object literals in assignmentsMulti-line preferred when fields might growSingle-line OK if short and stable

Return Objects

Return statements with objects must always be multi-line. This applies to success returns, error returns, and any return { ... } pattern.

javascript
return {
  success: false,
  items: [],
  count: 0,
  error: { type: 'QUERY_ERROR', message: error.message }
};

Control Flow

RuleApplies to
Block statements alwaysAll if statements use {} braces - no inline if (cond) doStuff();
Explicit returnsAlways use the return keyword - no implicit returns from arrow functions where return value matters

Data Output

RuleExample
Snake case for output JSONuser_agent, ip_address, created_at (never userAgent)
Named undefined params/* id */ undefined, // ID not yet assigned when passing positional undefined

Error Handling Disposal

Three categories, three disposal mechanisms. Never mix them. Full rule with rationale and worked examples: error-handling.md.


Performance Logging

performanceAuditLog is a general timing instrument - it measures how long a unit of work took. It has two sanctioned uses: external service operations (database, cloud API, HTTP, queue), where it is mandatory, and significant in-process work (batch analysis over large datasets, heavy transformations, algorithm phases), where it is used whenever duration insight has value.

javascript
const start_ms = Lib.Utils.getUnixTimeInMilliSeconds();
const response = await cloud_client.send(command);
Lib.Debug.performanceAuditLog('End', 'ServiceName Operation - ' + identifier, start_ms);

Rules:

  • Built-in instrumentation is part of every driver's contract. The service-boundary driver helpers (nosql-*, sql-*, queue-*, storage-*, http) log every roundtrip and every client/connection initialization themselves. This is an architecture assumption stated once, here: callers rely on it, and individual module docs do not restate it
  • Each interval is logged exactly once, by the layer that owns the work. Never instrument an interval whose work is performed by a module you call - the callee's built-in instrumentation already logs it. Wrapping a driver call in a second audit line one level up reports the same interval plus ~1 ms of in-process glue: duplicate noise, no signal. Store adapters and composite/parent modules therefore typically have zero calls; they add one only for their own substantial in-process work (a merge, an analysis pass, a large transformation), with a routine name that describes that work - never one that wraps a delegated call. A multi-roundtrip composite operation is visible in the logs as its sequence of driver lines; do not add a wrapper line to sum them
  • Use Lib.Debug.performanceAuditLog(action, routine, reference_time) - it calculates elapsed_ms and includes memory usage. The signature takes exactly three arguments; there is no fourth
  • One call per operation, after it completes, with action: 'End'. Do not emit 'Start'/'End' pairs - the reference time carries the start, so a second log line adds noise without adding signal
  • reference_time is the operation's own start time - a local start_ms captured at operation entry via Lib.Utils.getUnixTimeInMilliSeconds(), so elapsed_ms reports the operation's actual duration. Never pass instance['time_ms'] - it is the request-start timestamp, constant for the life of the request, so elapsed_ms would report request age instead of operation duration. Request-level timing is helper-instance's job (Instance.getAge), not the audit line's
  • Client/SDK initialization must log performance (import + connect time matters) using the same shape: capture init_start_ms before the import/connect work, emit one 'End' call after it. Never pass a timestamp created on the same line as the call - elapsed_ms would always be ~0
  • Error logs must include performance data - duration on failure helps diagnose timeouts

JSDoc Style

Every exported function carries a JSDoc block. Document the action, then the parameters, then the return shape.

JSDoc Block Conventions

  • Open with a one-sentence summary stating the action the function performs
  • Optional second paragraph for non-obvious behavior, examples, or syntax notes
  • @param lines list every parameter in signature order with type and one-line purpose
  • @return (singular, not @returns) describes the return shape; for object returns, list the keys inline (not as separate * @return sub-fields)

Body Indentation

All lines inside a JSDoc block (description, @param, @return, notes, and the closing *****/) start at the same column as the /* delimiter. The indentation is relative to the /* column, not absolute: a JSDoc block at column 0 has all content at column 0; a JSDoc block nested inside an object literal at column 4 has all content at column 4. Continuation lines (multi-line descriptions aligned for readability beyond /* col + 4) are left as-is. This matches every reference module (money.js, utils.js, debug.js).

Nested Object Params and Returns

Use JSDoc dot-notation - one @param or @return line per nested field. Never use custom * @param sub-indentation.

javascript
/********************************************************************
Validate every key in the options map.

@param {Object} [options] - Map of option names to their rule definitions
@param {Set} options[key].error - Error object for this key
@param {Boolean} [options[key].not_null] - (Optional) Reject null values

@return {Object} - Result data object
@return {String} .name - Name of the item
@return {String[]} .tags - List of associated tags
*********************************************************************/

When the JSDoc block is nested inside an indented context (e.g. a method inside an object literal), the /* and all content share the same column:

javascript
  const Interface = {

    /********************************************************************
    Return the current value.

    @return {String} - The current value
    *********************************************************************/
    getValue: function () {

      return this.value;

    }

  };

Comment Style

Write comments as a teammate would explain the line, not as marketing or reference-manual prose.

Voice and Tone

  • Prescriptive voice: "Run the query", "Return a service error if the driver call failed", "Build pool on first call"
  • One idea per comment. Split multi-idea sentences into separate lines or remove the redundant half
  • State the why when it is not obvious from the code; skip the comment when the code already says it
  • No vendor-specific examples in framework-level docs; vendor names belong in parenthetical clarifications only (e.g. Serverless function (Lambda, Cloud Function))
  • No migration breadcrumbs, no "legacy" labels, no references to previous codebases - that context belongs in __dev__/migration-changelog.md

Inline Step Comments Inside Functions

  • Every logical block within a function gets a single-line comment explaining what the next 2-5 lines do
  • The first logical block after the opening { starts with a one-line step comment (// Build pool on first call, // Start performance timeline, ...)
  • Every subsequent block separated by a blank line also gets a one-line comment
  • Comments describe intent, not syntax: prefer // Pick the first column of the row over // Get keys[0]
  • Inside try/catch, the catch block's first comment explains the fallback behavior, not that the try failed
  • No exceptions for short functions. Even a single-block function with one await and one if still gets its opening step comment
  • Use plain, direct language in comments: // Return a service error if the driver call failed not // Bubble up the error
  • A loop body is not one block. The comment above the loop states what the iteration accomplishes. Once the body carries more than two operations, each distinct operation inside it gets its own step comment, separated by a blank line. A body of one or two tight operations stays dense under the loop's own comment

The loop's own comment covers the sweep; the operations inside it are commented individually:

javascript
// Count and remove all handlers
let count = 0;
const ids = Object.keys(state.handlers);

for (let i = 0; i < ids.length; i++) {
  const id = ids[i];

  // Clear any pending timer for this handler
  if (state.handlers[id].timeout_id) {
    clearTimeout(state.handlers[id].timeout_id);
  }

  // Remove the handler from the registry
  delete state.handlers[id];

  // Count the removal
  count += 1;
}

Collapsing those three operations into an uncommented body is the drift this rule catches. The outer comment reads as though it covers them, so the body escapes review even though it is the part doing the work.

Every block has a step comment, even a short function:

javascript
removeItem: async function (instance, id) {

  // Delete the row by primary key
  const result = await Lib.DB.write(
    instance,
    'DELETE FROM items WHERE id = ?',
    [id]
  );

  // Return a service error if the driver call failed
  if (result.success === false) {
    Lib.Debug.debug('removeItem failed', { ... });
    return { success: false, error: ERRORS.SERVICE_UNAVAILABLE };
  }

  // Report success
  return { success: true, error: null };

},

Mandatory Step-Comment Set for I/O Functions

Every public function that performs I/O (database, network, file, queue, external SDK) carries AT MINIMUM a step comment on each of these blocks, whichever of them the function contains:

  1. Validate step. The call into the validators singleton
  2. Init step. The lazy-init or ensure call (initIfNot, ensureAdapter, pool build)
  3. Each driver or delegate call. Every line that crosses into a vendor library or another module
  4. Every success return. Each return { success: true, ... } block
  5. Every error return. Each return { success: false, ... } block, including the catch-block return
  6. Every early-return branch. Idempotency short-circuits, not-found paths, no-op guards

This set is the audit floor, not the ceiling: blocks outside this list still follow the universal every-logical-block rule above. The set exists so a reviewer or verification gate can check comment coverage mechanically: locate the six block kinds, confirm each is preceded by a comment. The step-comment conformance gate in the build and audit workflows checks exactly this set (see the workflow archetypes); the worked function body in the factory skeleton demonstrates it.

Adapter and Driver Lazy-Load Pattern

  • Third-party drivers, SDKs, and native clients are cached at module scope via a private helper named ensureAdapter()
  • The first call to any function that needs the external library calls ensureAdapter() first
  • Use ensureAdapter for every multi-instance module that wraps a vendor library (MySQL, Postgres, MongoDB, AWS SDK, ...) - do not invent a new name per module

Spelling and Prose Quality

These rules apply to every file the AI or human writes - .js comments and strings, .md documentation, package.json descriptions, README.md, ROBOTS.md, workflow files, and commit messages.

RuleCorrectIncorrect
American English (z not s)initialize, standardize, optimize, organize, centralize, authorize, specialize, cataloginitialise, standardise, optimise, organise, centralise, authorise, specialise, catalogue
American English (or not our)behavior, color, favorbehaviour, colour, favour
American English (ize not ise)optimization, organizationoptimisation, organisation
American English (license)licenselicence
No em-dashes- description or word - word in all filesword — word (Unicode U+2014) in any file type
No Unicode arrows in code files-> in .js comments and strings is forbidden in .js files. IS allowed in .md documentation (reduces tokens, improves clarity)
No em-dash as list-item separator**Term.** Explanation sentence.**Term** — explanation

Dependencies

RuleDetail
Minimize external depsUse built-in Node.js APIs. Add external libraries only when no built-in covers the need.
Wrap all librariesAll external libraries are wrapped in helper modules. No direct imports in business logic
Reuse Lib.UtilsBefore writing a utility (type check, validation, sanitization), check if Lib.Utils already provides it
Pin to verified latestVerify the current latest version with Context7 MCP and npm view <pkg> version before locking a range
Declare engines.nodeEvery module's package.json declares the minimum Node.js version it supports
No keywords fieldOmit keywords from package.json entirely

Full publishing pipeline: publishing.md. Peer dependency strategy: dependencies.md.


NPM Aliases in import and Error Prefixes

NPM Package Aliases

All internal cross-module references use npm aliases in package.json so source code stays free of the @superloomdev/ scope. This applies to both _test/package.json dependencies and the main package.json peerDependencies.

Alias derivation rule. Take the full package short-name and strip only two things:

  1. The leading js- (the language is obvious - everything in this project is JavaScript).
  2. The platform qualifier server- or client- that follows it (the directory category name (helper-modules-server, helper-modules-client) already makes the platform obvious).

Everything else stays in the alias - family segments (sql-, nosql-), cloud-vendor segments (aws-), and adapter parents (auth-store-, verify-store-, logger-store-). These segments carry real information and would create collisions or ambiguity if dropped. Module nomenclature itself lives in module-classes.md.

Full package nameAlias
js-helper-utilshelper-utils
js-server-helper-sql-sqlitehelper-sql-sqlite
js-server-helper-nosql-aws-dynamodbhelper-nosql-aws-dynamodb
js-server-helper-storage-aws-s3helper-storage-aws-s3
js-server-helper-auth-store-postgreshelper-auth-store-postgres
js-server-helper-cryptohelper-crypto
js-client-helper-cryptohelper-crypto (same alias)

Server and client variants of the same module share one alias - package.json decides which version installs and the calling code never branches on platform.

json
{
  "peerDependencies": {
    "helper-utils": "npm:@superloomdev/js-helper-utils@^1.0.0",
    "helper-sql-sqlite": "npm:@superloomdev/js-server-helper-sql-sqlite@^1.0.0",
    "helper-crypto": "npm:@superloomdev/js-server-helper-crypto@^1.0.0"
  }
}
javascript
import UtilsFactory from 'helper-utils';
import sqlSqlite from 'helper-sql-sqlite';
import crypto from 'helper-crypto';

Lib.Utils = UtilsFactory();
Lib.SQLite = sqlSqlite(Lib, config);
Lib.Crypto = crypto(Lib, config);

Error Message Prefixes

Error messages start with the same short name used in the alias, in square brackets. The derivation rule is identical - strip js- and the server-/client- qualifier, keep every other segment.

javascript
throw new Error('[helper-money] CONFIG.DEFAULT_CURRENCY_CODE is not a known currency');
throw new TypeError('[helper-auth] verifySession requires options.tenant_id');
throw new Error('[helper-nosql-aws-dynamodb] table_name is required');

AWS and Cloud SDK Modules

When writing helper modules that wrap AWS or other cloud SDKs:

RuleDetail
3-layer DRYBuilder (pure, no I/O) → Command Executor (I/O) → Convenience (calls both). Convenience functions internally use commandBuilder + commandExecutor. Builders are also used by transaction functions
Explicit credentialsAlways pass KEY and SECRET from CONFIG - never rely on the implicit credential chain inside module code. Loader injects credentials
Descriptive SDK variable namesName imports after the service - S3Client, DynamoDB, SQSClient - never lib, sdk, or single letters
ensureAdapter()Loads the vendor SDK on first call. Module-scoped, shared across instances because the adapter is stateless
initIfNot()Builds the per-instance resource (pool, client, connection) on first call. Calls ensureAdapter() first
Guard with Lib.Utils.isNullOrUndefinedBoth lazy-load helpers guard their cache check with this helper - never inline if (x !== null) return
Pure config filesConfig files contain only defaults - no process.env reads. Environment values are injected by the test loader or project loader
Reserved keywordsCloud services may have reserved words in query/expression languages. Always use aliasing (e.g., expression attribute names) to avoid conflicts with common field names like name, status, data, type
Batch API limitsCloud APIs impose batch size limits. Handle large batches with recursive chunking - split, process sequentially, combine results
Service-specific optionsConfigure marshalling, serialization, and retry behavior appropriate to the cloud service (e.g., removing undefined values, retry config)

Full module structure templates (Pattern 1 Singleton vs Pattern 2 Factory) live in module-structure.md under "Helper Module Configuration Patterns".

Released under the MIT License.