Expose a target
A target exposes a product on a specific platform, runtime, environment.
Its logical representation is a directory named src/products/{ProductName}/{targetname}
that has the following structure :
📄 container.ts
📄 index.ts(x)
📄 settings.ts
container
libmodulor
relies on inversify
to handle dependency injection.
Each target provides a container
binding everything necessary for the apps to work.
A very basic container
that works on all platforms looks like this.
const container = new Container(CONTAINER_OPTS);
bindCommon(container, settings);
bindProduct(container, Manifest, I18n);
export default container;
Based on the targets, additional bindings need to be performed in order to make things work.
The order in which bindings are defined is very important.
bindNodeCore
bindNodeCore
binds all the standard interfaces used internally by libmodulor
to Node.js implementations.
To be used in any target running on Node.js (e.g. server
, cli
, mcp-server
, etc.).
bindCommon(container, settings);
bindNodeCore(container);
bindNodeCLI
bindNodeCLI
binds all the standard interfaces used internally by libmodulor
to Node.js CLI implementations.
To be used in any target running on Node.js exposing a cli
.
bindCommon(container, settings);
bindNodeCLI(container);
bindRN
bindRN
binds all the standard interfaces used internally by libmodulor
to React Native implementations.
To be used in any target running on React Native.
bindCommon(container, settings);
bindRN(container);
bindServer
bindServer
binds all the standard interfaces used internally by libmodulor
NodeExpressServerManager
to basic implementations.
bindCommon(container, settings);
bindServer(container);
bindWeb
bindWeb
binds all the standard interfaces used internally by libmodulor
to web implementations.
bindCommon(container, settings);
bindWeb(container);
Custom bindings
If you're writing a complete product with libmodulor
, there are great chances you need to bind your own implementations.
As an example, let's say you are writing a product that allow people to call an LLM.
Typically, there would be an LLMManager
interface in the lib
folder, with multiple implementations : OpenAILLMManager
, MistralAILLMManager
, etc.
In the container
you would declare it this way.
container.bind<LLMManager>('LLMManager').to(MistralAILLMManager);
You can also override the bindings done previously by using rebind
.
container.rebind<SettingsManager>('SettingsManager').to(MyOwnVerySpecificSettingsManager);
index
It represents the enrypoint of the target. This file is very specific to what the target is.
Here are some examples.
import container from './container.js';
await container.get<I18nManager>('I18nManager').init();
await container.resolve(ServerBooter).exec({});
import App from './components/App.js';
import container from './container.js';
ReactDOM.createRoot(rootElt).render(
<StrictMode>
<DIContextProvider container={container}>
<App />
</DIContextProvider>
</StrictMode>,
);
import container from './container.js';
await container.get<I18nManager>('I18nManager').init();
await container.resolve(NodeCoreCLIManager).exec({});
import container from './container.js';
await container.get<I18nManager>('I18nManager').init();
await container.resolve(MCPServerBooter).exec({});
import App from './components/App.js';
import container from './container.js';
function Index(): ReactElement {
return (
<SafeAreaView>
<DIContextProvider container={container}>
<App />
</DIContextProvider>
</SafeAreaView>
);
}
registerRootComponent(Index);
How to "invoke" this entrypoint depends on a lot of parameters we don't cover here.
As a reminder, libmodulor
does not make any assumptions on the technical side.
Therefore, you can start your server
with node
, deno
or bun
, etc.
You can bundle your spa
with webpack
or vite
, etc.
You can start your rn
app with react-native-cli
, expo
, re.pack
, etc.
settings
In libmodulor
settings are modular.
Anything that needs settings, provides an interface that extends the Settings
interface.
export interface OpenAILLMManagerSettings extends Settings {
oai_api_key: ApiKey;
}
type S = OpenAILLMManagerSettings;
A product needs to compose all the settings required by all the elements it embeds. Typically, a product settings looks like this.
export type S = LoggerLevel & OpenAILLMManagerSettings & ServerManagerSettings; // & ...
export const settings: SettingsFunc<S> = (_common) => ({
...TARGET_DEFAULT_SERVER_MANAGER_SETTINGS,
logger_level: 'debug',
oai_api_key: 'MyAPIKey',
});
Hardcoding and committing to SCM regular settings like logger_level
is fine.
But it's not for sensitive settings like oai_api_key
.
Instead, this value needs to come from the environment.
To handle this case, override the SettingsManager
binding.
container.rebind<SettingsManager>('SettingsManager').to(EnvSettingsManager);
Change the value of the oai_api_key
setting.
oai_api_key: 'MyAPIKey',
oai_api_key: SettingsManagerMandatoryPlaceholder,
Introduce a .env
file and add it to .gitignore
.
app_oai_api_key=MySuperSecretAPIKey
Finally, when you start your server, provide this env file.
node --env-file .env index.ts
This is just an example.
As an alternative, you could provide an implementation named VaultSettingsManager
that implements the SettingsManager
interface.
This implementation would fetch the secrets from Vault by Hashicorp instead of environment variables.
Again, libmodulor
provides basics, but you can extend it the way you want.