# Module Structure & Examples Slothlet supports sophisticated module organization patterns with seamless ESM/CJS interoperability. This guide demonstrates the various ways you can structure your API modules and how they map to the resulting API structure. ## Overview Slothlet's module loader automatically transforms your file structure into a clean, intuitive API. It handles mixed ESM/CJS modules, automatic flattening, smart naming, and various export patterns - with no configuration required for common cases. ## Table of Contents - [Loading Pipeline Overview](#loading-pipeline-overview) - [Root-Level Modules](#root-level-modules) - [Filename-Folder Matching Modules](#filename-folder-matching-modules) - [Multi-File Modules](#multi-file-modules) - [Function-Based Modules](#function-based-modules) - [Mixed ESM/CJS Modules](#mixed-esmcjs-modules) - [Hybrid Export Patterns](#hybrid-export-patterns) - [Nested Structure](#nested-structure) - [Hidden Entries](#hidden-entries) - [Utility Modules](#utility-modules) - [Smart Function Naming](#smart-function-naming) - [TypeScript Modules](#typescript-modules) --- ## Loading Pipeline Overview The diagram below sketches the end-to-end flow: how a folder of `.mjs` / `.cjs` files becomes a usable API, how the `mode` choice (eager vs lazy) changes call-time behavior, and which features apply regardless of mode (live bindings, smart naming, mixed-module support). See [PERFORMANCE.md](PERFORMANCE.md) for the numbers and [CONFIGURATION.md](CONFIGURATION.md#mode) for the `mode` option reference. ```mermaid flowchart TD MODULEFOLDERS --> SLOTHLET SLOTHLET --> CHOOSEMODE CHOOSEMODE --> LAZY CHOOSEMODE --> EAGER subgraph EAGER ["⚡ Eager Mode"] direction TB EAGER0 ~~~ EAGER1 EAGER2 ~~~ EAGER3 EAGER0@{ shape: braces, label: "📥 All modules loaded immediately" } EAGER1@{ shape: braces, label: "✅ API methods available right away" } EAGER2@{ shape: braces, label: "🔄 Function calls behave as originally defined" } EAGER3@{ shape: braces, label: "📞 Sync stays sync: api.math.add(2,3)
🔄 Async stays async: await api.async.process()" } end subgraph LAZY ["💤 Lazy Mode"] direction TB LAZY0 ~~~ LAZY1 LAZY2 ~~~ LAZY3 LAZY4 ~~~ LAZY5 LAZY0@{ shape: braces, label: "📦 Modules not loaded yet" } LAZY1@{ shape: braces, label: "🎭 API methods are placeholders/proxies" } LAZY2@{ shape: braces, label: "📞 First call triggers materialization" } LAZY3@{ shape: braces, label: "⏳ All calls must be awaited
await api.math.add(2,3)" } LAZY4@{ shape: braces, label: "💾 Module stays loaded after materialization
Copy-left materialization" } LAZY5@{ shape: braces, label: "🚀 Subsequent calls nearly as fast as eager mode" } end subgraph EAGERCALL ["⚡ Eager Mode Calls"] direction TB end subgraph LAZYCALL ["💤 Lazy Mode Calls"] direction TB LAZYCALL0 --> LAZYCALL2 LAZYCALL0@{ shape: rounded, label: "📞 First call" } LAZYCALL1@{ shape: rounded, label: "🔁 Sequential calls" } LAZYCALL2@{ shape: rounded, label: "🧩 Materialize" } end EAGER --> READYTOUSE LAZY --> READYTOUSE READYTOUSE --> CALL CALL -.-> EAGERCALL CALL -.-> LAZYCALL EAGERCALL --> MATERIALIZEDFUNCTION LAZYCALL1 --> MATERIALIZEDFUNCTION LAZYCALL2 --> MATERIALIZEDFUNCTION READYTOUSE@{ shape: rounded, label: "🎯 Ready to Use" } MATERIALIZEDFUNCTION@{ shape: rounded, label: "✅ Materialized method/property" } CALL@{ shape: trap-b, label: "📞 Call" } subgraph ALWAYS ["✨ Extras Always On"] direction TB ALWAYS0 ~~~ ALWAYS1 ALWAYS1 ~~~ ALWAYS2 ALWAYS0@{ shape: rounded, label: "🔗 Live Bindings ALS
Per-instance context isolation" } ALWAYS1@{ shape: rounded, label: "🏷️ Smart Naming & Flattening
Multiple rules for clean APIs" } ALWAYS2@{ shape: rounded, label: "🔄 Mixed Module Support
Seamlessly mix .mjs and .cjs" } end MODULEFOLDERS@{ shape: st-rect, label: "📁 Modules Folder
.mjs and/or .cjs files
math.mjs, string.cjs, async.mjs" } SLOTHLET@{ shape: rounded, label: "🔧 Call slothlet(options)" } CHOOSEMODE@{ shape: diamond, label: "Choose Mode
in options" } style EAGER0 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style EAGER1 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style EAGER2 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style EAGER3 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style LAZY0 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style LAZY1 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style LAZY2 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style LAZY3 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style LAZY4 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style LAZY5 stroke:#9BC66B,color:#9BC66B,opacity:0.5 style MODULEFOLDERS fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style SLOTHLET fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style CHOOSEMODE fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style READYTOUSE fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style CALL fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style MATERIALIZEDFUNCTION fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style EAGER fill:#0d1a0d,stroke:#9BC66B,stroke-width:3px,color:#9BC66B,opacity:0.5 style EAGERCALL fill:#0d1a0d,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5 style LAZY fill:#0d1a0d,stroke:#B8D982,stroke-width:3px,color:#B8D982,opacity:0.5 style LAZYCALL fill:#0d1a0d,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5 style LAZYCALL0 fill:#1a1a1a,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5 style LAZYCALL1 fill:#1a1a1a,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5 style LAZYCALL2 fill:#1a1a1a,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5 style ALWAYS fill:#0d1a0d,stroke:#7FA94F,stroke-width:3px,color:#7FA94F,opacity:0.5 style ALWAYS0 fill:#1a1a1a,stroke:#7FA94F,stroke-width:1px,color:#7FA94F,opacity:0.5 style ALWAYS1 fill:#1a1a1a,stroke:#7FA94F,stroke-width:1px,color:#7FA94F,opacity:0.5 style ALWAYS2 fill:#1a1a1a,stroke:#7FA94F,stroke-width:1px,color:#7FA94F,opacity:0.5 linkStyle default stroke:#9BC66B,stroke-width:3px,opacity:0.5 linkStyle 4,5,6,7,8,18,19 stroke-width:0px ``` --- ## Root-Level Modules Files at the root of your API directory become top-level API properties. Dash-separated filenames are converted to camelCase: ```text api/ ├── root-math.mjs → api.rootMath ├── rootstring.mjs → api.rootstring └── config.mjs → api.config ``` ```javascript // api/root-math.mjs export function add(a, b) { return a + b; } // Usage const api = await slothlet({ dir: "./api" }); const result = api.rootMath.add(2, 3); // 5 ``` --- ## Filename-Folder Matching Modules When a folder contains a single file whose name matches the folder name, slothlet automatically flattens the structure - the intermediate namespace is eliminated: ```text api/ ├── math/ │ └── math.mjs → api.math (not api.math.math) ├── string/ │ └── string.mjs → api.string └── util/ └── util.cjs → api.util ``` ```javascript // api/math/math.mjs export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } // Usage const api = await slothlet({ dir: "./api" }); const sum = api.math.add(2, 3); // ✅ not api.math.math.add() const diff = api.math.subtract(5, 2); ``` See [API Flattening Rules](API-RULES/API-FLATTENING.md) for the full set of flattening patterns. --- ## Multi-File Modules Folders with multiple files create namespaced API structures. Each file becomes a sub-key: ```text api/ └── multi/ ├── alpha.mjs → api.multi.alpha ├── beta.mjs → api.multi.beta └── gamma.cjs → api.multi.gamma ``` ```javascript // api/multi/alpha.mjs export function processAlpha(data) { return `Alpha: ${data}`; } // api/multi/beta.mjs export function processBeta(data) { return `Beta: ${data}`; } // api/multi/gamma.cjs function processGamma(data) { return `Gamma: ${data}`; } module.exports = { processGamma }; // Usage const api = await slothlet({ dir: "./api" }); api.multi.alpha.processAlpha("test"); api.multi.beta.processBeta("test"); api.multi.gamma.processGamma("test"); // CJS works seamlessly ``` --- ## Function-Based Modules Modules that export a default function become callable directly at their API path: ```text api/ ├── funcmod/ │ └── funcmod.mjs → api.funcmod() (callable) └── multi_func/ ├── alpha.mjs → api.multi_func.alpha() └── beta.cjs → api.multi_func.beta() ``` ```javascript // api/funcmod/funcmod.mjs export default function(name) { return `Hello, ${name}!`; } // api/multi_func/alpha.mjs export default function(value) { return value * 2; } // api/multi_func/beta.cjs module.exports = function(value) { return value * 3; }; // Usage const api = await slothlet({ dir: "./api" }); api.funcmod("Alice"); // "Hello, Alice!" api.multi_func.alpha(5); // 10 api.multi_func.beta(5); // 15 ``` --- ## Mixed ESM/CJS Modules ESM and CJS modules coexist transparently in the same directory. CJS `module.exports` is treated equivalently to an ESM `export default` - no `.default` wrapper is introduced: ```text api/ └── interop/ ├── esm-module.mjs → api.interop.esmModule ├── cjs-module.cjs → api.interop.cjsModule └── mixed.mjs → api.interop.mixed ``` ```javascript // api/interop/esm-module.mjs export function esmFunction(data) { return `ESM: ${data}`; } // api/interop/cjs-module.cjs function cjsFunction(data) { return `CJS: ${data}`; } module.exports = { cjsFunction }; // api/interop/mixed.mjs - can call both through self import { self } from "@cldmv/slothlet/runtime"; export function callBoth(data) { const esmResult = self.interop.esmModule.esmFunction(data); const cjsResult = self.interop.cjsModule.cjsFunction(data); return { esmResult, cjsResult }; } // Usage const api = await slothlet({ dir: "./api" }); api.interop.esmModule.esmFunction("test"); api.interop.cjsModule.cjsFunction("test"); api.interop.mixed.callBoth("test"); ``` > **CJS default exports**: `module.exports = { fn }` is always accessible directly as `api.module.fn` - never as `api.module.default.fn`. Slothlet normalizes the CJS `default` wrapper so CJS and ESM modules have identical access patterns. --- ## Hybrid Export Patterns Modules can export both a default function and named properties. The default function becomes the callable entry point; named exports become methods on it: ```text api/ └── exportDefault/ └── exportDefault.mjs → api.exportDefault() (callable with .info / .error methods) ``` ```javascript // api/exportDefault/exportDefault.mjs export default function logger(msg) { console.log(msg); } logger.info = (msg) => console.log(`[INFO] ${msg}`); logger.error = (msg) => console.error(`[ERROR] ${msg}`); export { logger }; // Usage const api = await slothlet({ dir: "./api" }); api.exportDefault("Hello"); // Direct call api.exportDefault.info("Info"); // Method call api.exportDefault.error("Error"); // Method call ``` --- ## Nested Structure Slothlet supports arbitrarily deep directory structures: ```text api/ ├── nested/ │ └── date/ │ ├── date.mjs → api.nested.date │ └── util.cjs → api.nested.dateUtil └── advanced/ └── self-object/ → api.advanced.selfObject ``` ```javascript // api/nested/date/date.mjs export function formatDate(date) { return date.toISOString(); } // api/nested/date/util.cjs function parseDate(str) { return new Date(str); } module.exports = { parseDate }; // Usage const api = await slothlet({ dir: "./api" }); const formatted = api.nested.date.formatDate(new Date()); const parsed = api.nested.dateUtil.parseDate("2025-12-30"); ``` --- ## Hidden Entries Files **and folders** whose names start with `.` or `__` are excluded from the API scan — use the `__` prefix for JSDoc-only helpers, scratch folders, or anything that should live inside the API tree without becoming an endpoint. Additional entries can be hidden per project with the [`hidden` config option](./CONFIGURATION.md#hidden) (globs relative to the API root, also accepted per-call by `api.slothlet.api.add`). A folder that ends up with no loadable contents — empty, or everything inside it hidden — creates no API key at all. If you depended on the pre-v3.11 behavior where `.`/`__`-prefixed **folders** were still scanned, the deprecated [`scanHiddenFolders`](./CONFIGURATION.md#scanhiddenfolders) escape hatch restores it while you migrate (removed in v4). --- ## Utility Modules Utility modules follow the same structural rules. Mixed ESM/CJS works at any depth: ```text api/ └── util/ ├── controller.mjs → api.util.controller ├── extract.cjs → api.util.extract └── url/ ├── parser.mjs → api.util.url.parser └── builder.cjs → api.util.url.builder ``` ```javascript // api/util/controller.mjs export function handleRequest(req) { return { status: 200, data: req }; } // api/util/extract.cjs function extractData(obj, key) { return obj[key]; } module.exports = { extractData }; // api/util/url/parser.mjs export function parseUrl(url) { return new URL(url); } // api/util/url/builder.cjs function buildUrl(base, path) { return `${base}/${path}`; } module.exports = { buildUrl }; // Usage const api = await slothlet({ dir: "./api" }); api.util.controller.handleRequest({ data: "test" }); api.util.extract.extractData({ key: "value" }, "key"); api.util.url.parser.parseUrl("https://example.com"); api.util.url.builder.buildUrl("https://example.com", "path"); ``` --- ## Smart Function Naming Slothlet preserves function name capitalization for technical acronyms. When a module's exported function name contains an acronym (IP, JSON, HTTP, API, etc.), Slothlet uses the function's own name as the API key rather than deriving it from the filename: ```text api/ ├── task/ │ └── auto-ip.mjs → api.task.autoIP (not autoIp) ├── util/ │ └── parseJSON.mjs → api.util.parseJSON (not parseJson) └── api/ └── getHTTPStatus.mjs → api.api.getHTTPStatus ``` ```javascript // api/task/auto-ip.mjs - function name takes precedence over filename export function autoIP(config) { return "192.168.1.1"; } // api/util/parseJSON.mjs export function parseJSON(str) { return JSON.parse(str); } // api/api/getHTTPStatus.mjs export function getHTTPStatus(code) { return code === 200 ? "OK" : "Error"; } // Usage const api = await slothlet({ dir: "./api" }); api.task.autoIP({ mode: "dhcp" }); // not .autoIp api.util.parseJSON('{"key": "value"}'); // not .parseJson api.api.getHTTPStatus(200); // not .getHttpStatus ``` --- ## TypeScript Modules Slothlet supports TypeScript modules with two transpilation strategies. TypeScript is a peer dependency - install only what you need. ### Fast Mode (esbuild) ```bash npm install esbuild ``` ```text api/ ├── math.ts → api.math └── utils.ts → api.utils ``` ```typescript // api/math.ts export function add(a: number, b: number): number { return a + b; } export function multiply(a: number, b: number): number { return a * b; } ``` ```javascript const api = await slothlet({ dir: "./api", typescript: { mode: "fast" } // uses esbuild }); api.math.add(2, 3); // 5 api.math.multiply(3, 4); // 12 ``` ### Strict Mode (tsc) ```bash npm install typescript ``` ```javascript const api = await slothlet({ dir: "./api", typescript: { mode: "strict" } // uses tsc, respects tsconfig.json }); ``` TypeScript modules (`.ts`) follow all the same structural rules as ESM modules - flattening, function naming, and nesting all behave identically. ### Runtime Imports from `.ts` / `.mts` `.ts` and `.mts` files can import `self`, `context`, and `instanceID` from `@cldmv/slothlet/runtime` exactly like `.mjs` files, along with other bare-specifier (package) imports. Slothlet writes the transpiled output to a project-local cache file so Node's resolver can anchor those imports normally. Relative imports also resolve normally: specifiers to plain `.mjs` / `.cjs` / `.js` files (`./helper.mjs`, `../shared/util.cjs`) are anchored at the original source directory, and relative imports of other `.ts` / `.mts` modules are transpiled and linked automatically — import cycles included. ```typescript // api/utils.ts import { self, context, instanceID } from "@cldmv/slothlet/runtime"; export function ping() { return `${instanceID}/${context?.requestId ?? "anon"} → ${self.math.add(1, 1)}`; } ``` > Earlier than 3.5.0, `.ts` modules could not resolve bare specifiers (the loader used `data:` URLs which Node's ESM resolver can't anchor against). See the v3.5.0 changelog for details. --- ## Key Principles 1. **Automatic Flattening**: When a folder name matches its single file's name, the intermediate namespace is eliminated 2. **Transparent CJS/ESM**: ESM and CJS modules have identical access patterns - no `.default` wrapper 3. **Smart Naming**: Exported function names (including acronyms) take precedence over filenames for the API key 4. **Flexible Exports**: Default exports, named exports, and hybrid patterns (callable + methods) are all supported 5. **Unlimited Depth**: No constraints on directory depth or complexity 6. **Universal Access**: All modules are accessible through the `self` live binding from any other module --- ## See Also - [API Flattening Rules](API-RULES/API-FLATTENING.md) - Detailed flattening logic and all 8 patterns - [API Rules](API-RULES.md) - Complete 13-rule transformation catalog - [Module Discovery + Mount](MODULE-DISCOVERY.md) - Composing modules from separate npm packages at runtime via the `api.slothlet.api.modules.*` pipeline - [README](../README.md) - Main project documentation