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 v9+Lint and auto-fixnpm run lint, npm run lint:fix
Flat configRequired by ESLint v9Every 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]

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]
'use strict';


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

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


/////////////////////////// Module-Loader START ////////////////////////////////
module.exports = 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 module.exports = function loader (...) - with no separate Module Exports section; splitting them adds a banner and an indirection without adding information. Application modules (entity controllers, services, models) keep the separate Module Exports 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

  • 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
  • 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

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) are indented 4 spaces from the column of the /* delimiter. This applies even when the JSDoc block sits inside an object literal that is itself indented 4 spaces.

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
*********************************************************************/

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 require() 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
Lib.Utils = require('helper-utils')();
Lib.SQLite = require('helper-sql-sqlite')(Lib, config);
Lib.Crypto = require('helper-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.