Node Utils Guide
Use this guide when you need Node.js-only helpers for FaasJS runtime bootstrapping, local tooling, config resolution, or logging.
Applicable Scenarios
- Running a handler, CLI, or test directly in Node.js
- Reading staged
faas.yamlconfiguration - Loading plugins, API handler modules, or packages dynamically
- Registering runtime module hooks for lifecycle or instrumentation
- Validating boundary inputs with Zod schema-based parsing
- Setting up a custom Node-side logger
What @faasjs/node-utils Gives You
- config loading:
loadConfig - API loading:
loadApiHandler,loadPlugins - Node module bootstrapping:
loadPackage,registerNodeModuleHooks,resetRuntime - filesystem containment checks:
isPathInsideRoot(see Validation Guide) - schema parsing:
parseSchemaValue,formatSchemaError,SchemaOutput(see Validation Guide) - logging and log shipping:
Logger,formatLogger,getTransport,Transport,colorize
Default Workflow
- Keep
@faasjs/node-utilsimports in Node-only entrypoints, tests, CLIs, or adapters. - Let
ServerorviteFaasJsServer()auto-load the project.envwhen they own bootstrap from a FaasJS app root, and call Node's built-inloadEnvFile()yourself infaas runentry files, plain Node scripts, tests, or other entrypoints that read env before those helpers start. - Use
loadConfig()when you only need stagedfaas.yamldata. - Use
parseYaml()from@faasjs/utilswhen you need the raw FaasJS YAML subset in custom tooling without staged discovery (see YAML Guide). - Use
loadApiHandler()when you need the final exported handler from a default-exported FaasJS API module, orloadPlugins()when you already have aFuncinstance. - Prefer the FaasJS TypeScript loader when direct Node execution must understand local TypeScript files or tsconfig aliases, keep local imports extensionless without
.tsor.tsxsuffixes, and use default exports for modules loaded throughloadPackage(). - Use
isPathInsideRoot()before reading or loading root-scoped files from user-controlled or URL-derived paths. - Use
parseSchemaValue()for custom Node-side boundaries that need the same optional Zod schema parsing and error formatting as FaasJS APIs and jobs. - Reuse
Loggerand the shared transport instead of building a custom logging wrapper.
Rules
1. Keep node-utils in Node-only code paths
- This package depends on Node APIs such as
node:module,node:process, and filesystem access. - Do not import it into browser code, React components, or code that must run on edge runtimes.
- Use
@faasjs/utilsfor portable helpers and@faasjs/coreor@faasjs/devfor framework/runtime primitives.
2. Load .env files early when your entrypoint needs them
@faasjs/coreServerand@faasjs/devviteFaasJsServer()already try to load the project.envbefore handlers run when they start from a FaasJS app root.loadEnvFile()fromnode:processis still the direct entrypoint forfaas runentry files, local scripts, tests, CLIs, and config files that readprocess.envbefore those helpers start.- Call it before reading
process.env, building config objects, or loading modules that depend on env values. - Keeping an explicit
loadEnvFile()inserver.tsis still a good default when your bootstrap does work beforenew Server(...)or when the same file can run throughfaas run. - Wrap it in
try/catchwhen the env file is optional and startup should continue without it.
import { loadEnvFile } from 'node:process'
try {
loadEnvFile()
} catch (error) {
console.warn('Failed to load env file', error)
}
3. Let loadConfig() resolve staged faas.yaml
- Do not reimplement directory walking or manual deep merging for
faas.yaml. loadConfig()walks from project root to the target API directory, merges nested files, appliesdefaults, and annotates plugin entries with their resolvedname.- Relative plugin
typevalues in YAML are resolved from thefaas.yamlfile that declared them, including./plugin.tsandfile://./plugin.ts. loadConfig()validates the YAML shape and preserves custom stage fields, but it does not instantiate plugin classes.- This keeps runtime behavior consistent with FaasJS plugin loading.
import { loadConfig } from '@faasjs/node-utils'
const config = loadConfig(process.cwd(), '/project/src/orders/create.api.ts', 'production')
console.log(config.plugins?.http)
4. Pick the smallest loader for the job
- Use
loadConfig()when you need stagedfaas.yamlresolution with directory walking and merging. - Use
loadApiHandler()when you need the final handler that a runtime or test will invoke; the API module must default-export a FaasJS API instance. - Use
loadPlugins()when you already have aFuncinstance and want YAML-driven plugins and config attached before exporting or mounting it. loadPlugins()merges YAML plugin config with inlinefunc.config.plugins; inline config wins, existing plugin instances are configured in place, and missing YAML plugins are instantiated.- Only
httpgets a built-in plugin type. Other YAML plugins need an explicittype, and config-driven plugin modules must default-export a plugin class withonMountoronInvoke. - Use
loadPackage()for default-export dynamic module loading in Node.js, especially when the target is a local TypeScript file or a path-alias-aware module. loadPackage()returns onlymodule.default; named exports are not fallback values.- Prefer these helpers over ad hoc
import()wrappers so cache busting, tsconfig resolution, and plugin wiring stay consistent.
import { loadEnvFile } from 'node:process'
import { loadApiHandler } from '@faasjs/node-utils'
loadEnvFile()
const handler = await loadApiHandler(
process.cwd(),
'/project/src/orders/create.api.ts',
process.env.FaasEnv || 'development',
)
const result = await handler(event, context)
5. Register module hooks only at process bootstrap
registerNodeModuleHooks()is for long-lived Node entrypoints such as CLIs, dev servers, or bootstrap scripts that need tsconfig path alias resolution.- Prefer the preload entry
node --import @faasjs/node-utils/register-hooks <entry>for direct Node execution so scripts keep standard extensionless local imports. - Call it once near startup. Repeated calls are safe, but scattering it across modules makes startup intent harder to follow.
- In isolated tests that depend on fresh loader state, call
resetRuntime()between cases instead of reinitializing the whole process.
import { registerNodeModuleHooks } from '@faasjs/node-utils'
registerNodeModuleHooks({
root: process.cwd(),
})
await import('./scripts/sync-users')
6. Keep Node-side logging on the shared primitives
- Use
Loggerfor structured levels, labels, timers, and environment-driven verbosity. - Use
getTransport()only when logs must be buffered and forwarded to another sink. Loggerdefaults toinfolevel, a 1000-character truncation threshold for debug/info output, auto-detected terminal colors, and shared transport forwarding outside Vitest.colorize()andformatLogger()are lower-level helpers; prefer theLoggerclass unless you are implementing logging infrastructure.colorize()always emits ANSI escapes; letLoggerdecide whether output should be colorized.
7. Use schema helpers for custom Node boundaries
- Use
parseSchemaValue()when a boundary has an optional Zod schema and needs consistent fallback and error formatting. - Without a schema, it returns
defaultValue(or{}) and ignores the raw value. - With a schema,
nullandundefinedare replaced bydefaultValue(or{}) beforesafeParseAsync(). - See Validation Guide for
parseSchemaValue,formatSchemaError, andSchemaOutputpatterns.
8. Validate root-scoped file paths with isPathInsideRoot()
See Validation Guide for isPathInsideRoot patterns.
Review Checklist
@faasjs/node-utilsimports stay in Node-only codefaas runentry files and local scripts load.envbefore env-dependent bootstrap logic unlessServerorviteFaasJsServer()fully owns that bootstrap- staged
faas.yamlis read throughloadConfig()orloadApiHandler(), not custom merge code - relative YAML plugin
typevalues are left forloadConfig()/loadPlugins()to normalize - loaders use
loadApiHandler(),loadPlugins(), orloadPackage()instead of custom dynamic import wrappers - modules loaded through
loadPackage()and config-driven plugin modules use default exports - non-
httpYAML plugins declare an explicittype - module hooks are registered at process startup, not deep inside feature code
- custom Node-side boundary validation uses
parseSchemaValue()(see Validation Guide) - root-scoped file access validates resolved paths with
isPathInsideRoot()(see Validation Guide) - tests that depend on fresh loader state use
resetRuntime() - logging uses
Loggeror the shared transport instead of rawconsolewrappers