libmodulor v0.18.1 is out 🚀 => Check it out on GitHub or npm !
libmodulor
Guides

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.

container.ts
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.).

container.ts
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.

container.ts
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.

container.ts
bindCommon(container, settings);
bindRN(container); 

bindServer

bindServer binds all the standard interfaces used internally by libmodulor NodeExpressServerManager to basic implementations.

container.ts
bindCommon(container, settings);
bindServer(container); 

bindWeb

bindWeb binds all the standard interfaces used internally by libmodulor to web implementations.

container.ts
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.ts
container.bind<LLMManager>('LLMManager').to(MistralAILLMManager);

You can also override the bindings done previously by using rebind.

container.ts
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.

server/index.ts
import container from './container.js';

await container.get<I18nManager>('I18nManager').init();
await container.resolve(ServerBooter).exec({});
web-react/index.tsx
import App from './components/App.js';
import container from './container.js';

ReactDOM.createRoot(rootElt).render(
    <StrictMode>
        <DIContextProvider container={container}>
            <App />
        </DIContextProvider>
    </StrictMode>,
);
cli/index.ts
import container from './container.js';

await container.get<I18nManager>('I18nManager').init();
await container.resolve(NodeCoreCLIManager).exec({});
mcp-server/index.ts
import container from './container.js';

await container.get<I18nManager>('I18nManager').init();
await container.resolve(MCPServerBooter).exec({});
rn/index.tsx
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.

OpenAILLMManager.ts
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.

server/settings.ts
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.

server/container.ts
container.rebind<SettingsManager>('SettingsManager').to(EnvSettingsManager);

Change the value of the oai_api_key setting.

server/settings.ts
    oai_api_key: 'MyAPIKey', 
    oai_api_key: SettingsManagerMandatoryPlaceholder, 

Introduce a .env file and add it to .gitignore.

server/.env
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.