tl;dr: There's no reason why you couldn't apply Dependency Injection patterns or export classes in Node.js.
The principal reason you doubt to export classes—even if they have a singleton lifetime scope in your application—is the hacky, dynamic nature of JavaScript.
Of course, you can actually do quite a lot without resorting to the Dependency Injection pattern.
For example, as you mentioned, you can export instances of your classes thus making them all singletons. The module exported in that way doesn't even have to be an instance of any class. Consider the following code snippets:
export class Foo {
foo() {
}
bar() {
}
}
export default new Foo();
versus:
export function foo() {
}
export function bar() {
}
Thus, this is a quite frequent way in JavaScript ecosystem. It's okay in all senses if there is no internal module state involved. Say, a library which public interface consists of a bunch of functions (like jquery, lodash, etc). But there is still the issue that some classes (Bar in your example) is responsible for fetching its own dependencies (Foo).
You can also use some hacks like proxyquire. It's the common way people solve the testing problem in JavaScript. With such libraries you can intercept calls to require (don't sure about support for ES6 module syntax like import, though) and provide your own mocks/stubs for testing purposes.
However, as an application becomes more complex and grows larger, DI can help the maintainability of your source code.
I think that the main problem of applying DI practices in JS is immaturity. None of the current DI solutions in JavaScript are really popular at the moment, so not so many guidelines, no tutorials, no infrastructure. Moreover, many libraries in JavaScript claiming to be a DI Containers are either polluting the modules with ugly annotations or actually implementing the Service Locator anti-pattern or anything.
So, I think, the most reasonable way is to:
- Export classes wherever possible. Exception: you can re-export classes as constructed instances in your application's Composition Root.
- Use Pure DI (without a DI Container) in not very complicated cases or consider usage of some DI Container (you have to google and make a thoughtful choice, though).
- Avoid overengineering. Don't hesitate, for example, to depend explicitly on some external libraries with stable API or your own modules in the application or infrastructure layers (hovewer, I'd recommend for your domain/business logic layer to stay clean).
Of course, as with anything related to software development, choosing between DI or exporting singletons depends on your software's complexity and your requirements.
process.env.NODE_PATHto my libraries directory, so I can useimportand have it be relative to that libraries directory by default. This is kind of like dependency injection and so far I haven't felt the need for real dependency injection in my node.js programming. But if you wanted to configure dependencies between modules in some central place then that could motivate using a real DI container system.