ow to structure a data processing pipeline that leverages each runtime’s strengths while maintaining cross-platform compatibility.
Step 1: Abstract Environment and I/O Boundaries
Runtime-specific globals (__dirname, process.cwd(), import.meta) behave inconsistently across execution environments. Abstract them early to prevent migration friction and ensure predictable path resolution.
// src/core/runtime-context.ts
export const RuntimeContext = {
getBaseDir: () => {
if (typeof import.meta.dirname !== 'undefined') {
return import.meta.dirname;
}
return process.cwd();
},
getEnvVar: (key: string, fallback?: string): string => {
return process.env[key] ?? fallback ?? '';
},
isProduction: () => {
return process.env.NODE_ENV === 'production' || process.env.DENO_ENV === 'production';
}
};
Each runtime requires distinct initialization flags and configuration files. The following examples demonstrate production-ready setups for a unified service interface.
Node.js 22 LTS Configuration
Node.js 22 introduces native TypeScript stripping and a stabilized test runner. This eliminates the need for ts-node or tsx in development while preserving full CommonJS/ESM interop.
# Execute with type stripping
node --experimental-strip-types src/pipeline.ts
# Run tests using native module
node --test src/__tests__/pipeline.test.ts
// src/__tests__/pipeline.test.ts
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
describe('DataPipeline', () => {
test('validates input schema before processing', async () => {
const payload = { id: 'evt_992', timestamp: Date.now() };
const isValid = await validatePayload(payload);
assert.strictEqual(isValid, true);
});
});
Bun 1.2 Configuration
Bun’s architecture (Zig + JavaScriptCore) enables synchronous I/O operations and a built-in bundler. Use the compilation flag for distributable CLI tools or edge functions where binary distribution is preferred over source deployment.
// build.config.ts
import { build } from 'bun';
await build({
entrypoints: ['./src/pipeline.ts'],
outdir: './dist/bun',
target: 'node',
minify: true,
naming: '[name].mjs'
});
# Compile to standalone binary
bun build --compile ./src/pipeline.ts --outfile pipeline-cli
./pipeline-cli --mode=production
Deno 2 Configuration
Deno 2 enforces explicit permissions and uses a global dependency cache. The npm: specifier bridges the existing ecosystem without node_modules, while deno.json centralizes task execution and type checking.
// deno.json
{
"imports": {
"pipeline-core": "./src/pipeline.ts",
"zod": "npm:zod@3.23.8"
},
"tasks": {
"start": "deno run --allow-net --allow-read=./data --allow-env=API_KEY src/pipeline.ts",
"check": "deno check src/pipeline.ts"
}
}
Step 3: Architecture Rationale
- Node.js 22 is selected when native addon compatibility (
node:crypto, node:child_process, .node binaries) or enterprise hiring pools are primary constraints. The --experimental-strip-types flag reduces startup overhead without altering the module resolution algorithm, making it the safest path for legacy migration.
- Bun 1.2 is chosen for latency-sensitive deployments. The JavaScriptCore engine avoids V8’s compilation overhead, yielding faster cold starts. The built-in bundler and package manager eliminate third-party toolchain dependencies, reducing CI surface area and dependency resolution bottlenecks.
- Deno 2 is optimal for security-compliant environments. The permission model prevents unauthorized filesystem or network access by transitive dependencies. Runtime type checking (
deno check) catches schema violations before execution, reducing production debugging cycles and enforcing strict contract validation.
Pitfall Guide
-
Assuming Native Addon Parity
Explanation: Bun and Deno do not fully support Node.js native C++ bindings (.node files). Packages like sharp, bcrypt, or sqlite3 may fail or fall back to slower WASM implementations.
Fix: Audit package.json for native dependencies. If critical, stick to Node.js 22. Otherwise, replace with pure-JS/WASM alternatives or use Bun’s experimental native addon support with explicit fallbacks.
-
Ignoring Explicit Permission Models
Explanation: Deno’s security model blocks filesystem and network access by default. Scripts that assume open access will throw PermissionDenied errors at runtime.
Fix: Map required permissions explicitly in deno.json tasks. Never use --allow-all in production. Use DENO_PERMISSIONS environment variables for dynamic CI environments.
-
Misinterpreting TypeScript Stripping vs. Checking
Explanation: Node.js 22 and Bun strip type annotations during execution but do not validate them. Runtime type errors will surface as undefined or TypeError instead of compile-time failures.
Fix: Run tsc --noEmit or deno check in CI pipelines regardless of runtime. Treat stripping as a development convenience, not a type safety guarantee.
-
Overlooking Global Dependency Caching
Explanation: Deno caches packages globally in DENO_DIR. This can cause version conflicts across projects or unexpected cache invalidation during updates.
Fix: Isolate cache directories per project using DENO_DIR=./.deno_cache. Commit deno.lock to version control to ensure reproducible builds.
-
Relying on Runtime-Specific Globals
Explanation: __dirname, __filename, and process.cwd() behave inconsistently across runtimes, especially in ESM modules.
Fix: Standardize on import.meta.dirname and import.meta.url. Abstract environment access behind a configuration module. Test across all target runtimes before deployment.
-
Misconfiguring Built-in Bundlers
Explanation: Bun’s bundler defaults to browser target, which breaks Node.js-specific APIs like fs or path.
Fix: Explicitly set target: 'node' in Bun.build configurations. Verify output by running the bundled file in a clean Node.js environment.
-
Neglecting CI/CD Runtime Parity
Explanation: Developing on Bun but deploying to Node.js (or vice versa) causes silent failures due to module resolution differences or missing globals.
Fix: Containerize applications with the exact runtime version. Use .tool-versions or package.json engines field. Run integration tests against the production runtime in CI.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Serverless/Edge Functions | Bun 1.2 | 6ms startup reduces cold-start latency and invocation costs | Lowers compute billing by 40-60% for sporadic workloads |
| Enterprise Legacy Migration | Node.js 22 LTS | Zero code changes, full npm compatibility, widest hiring pool | Near-zero migration cost, maintains existing CI/CD |
| Security-Compliant SaaS | Deno 2 | Explicit permissions prevent supply chain attacks, built-in tooling reduces config drift | Lowers security audit overhead, reduces vulnerability exposure |
| CLI Tools & Developer Utilities | Bun 1.2 | Sub-10ms startup improves developer experience, single binary compilation simplifies distribution | Reduces user friction, eliminates runtime installation requirements |
| Monorepo Workspaces | Bun 1.2 | 1.2s install time accelerates CI pipelines across 10+ packages | Cuts CI wait times by 70-80%, improves developer iteration speed |
Configuration Template
// project-root/runtime-config.json
{
"node": {
"version": "22.x",
"flags": ["--experimental-strip-types"],
"testRunner": "node:test",
"ciScript": "node --test src/__tests__/"
},
"bun": {
"version": "1.2.x",
"flags": [],
"bundler": {
"entrypoints": ["./src/index.ts"],
"outdir": "./dist",
"target": "node",
"minify": true
},
"ciScript": "bun test"
},
"deno": {
"version": "2.x",
"permissions": ["--allow-net", "--allow-read=./data", "--allow-env=API_KEY"],
"tasks": {
"start": "deno run --watch src/index.ts",
"check": "deno check src/index.ts"
},
"ciScript": "deno test --allow-all"
}
}
Quick Start Guide
- Initialize the project structure: Create
src/index.ts with a basic HTTP server or data processor. Abstract environment and path resolution using import.meta.dirname.
- Select your runtime: Install the target runtime (
nvm use 22, bun install, or deno install). Run node --experimental-strip-types, bun run, or deno run --allow-net respectively.
- Configure type validation: Add
tsc --noEmit to your CI pipeline, or use deno check for Deno. Ensure all type errors are caught before execution.
- Test cross-runtime compatibility: Run the same test suite across all three runtimes. Verify that module resolution, environment variables, and path handling behave identically.
- Deploy with parity: Containerize the application using the exact runtime version. Set explicit permissions (Deno) or bundler targets (Bun) in your deployment configuration. Validate in a staging environment before production rollout.