The Case for Capability-Based Dependencies
Standard Node.js development treats dependencies as a fixed, up-front cost. By importing packages directly (e.g., import sharp from "sharp"), applications become tightly coupled to specific libraries, forcing every deployment to carry the weight of every optional feature. This leads to bloated install sizes, slower cold starts, and unnecessary maintenance overhead. The core architectural shift is to move from package-based dependencies to capability-based dependencies, where features request a capability (e.g., "markdown") rather than a specific implementation.
Implementing On-Demand Injection
To implement this, decouple the feature from the package using a central registry. The architecture consists of five components:
- Module Manifest: A JSON file defining supported capabilities, their associated npm packages, versions, and entry points. This acts as a controlled vocabulary.
- Module Registry: A central resolver that validates requests against an allowlist, checks if a package is installed, performs dynamic imports, and caches the result.
- Injection Container: A simple interface that feature code uses to request capabilities (e.g.,
await modules.resolve("markdown")), hiding the underlying package logic. - Lazy Installer: A mechanism that triggers
npm installonly when a requested capability is missing. In production, this should be restricted to pre-fetching or controlled internal caches. - Security Gate: A mandatory layer that prevents arbitrary package installation. It must enforce an explicit allowlist, pinned versions, and lockfile integrity to prevent supply chain vulnerabilities.
Production Considerations
While lazy loading is beneficial, production environments require stricter controls. Use optionalDependencies in package.json for applications or peerDependencies with peerDependenciesMeta for libraries to signal optionality. To maintain performance in production, implement a prefetch command in your tooling to install required optional modules during the deployment phase, ensuring the runtime remains fast while keeping the architecture modular. This approach is particularly effective for plugin-based systems, large-scale applications with varied feature sets, and environments where cold start performance is critical.