file: ./content/docs/index.mdx
meta: {
"title": "Introduction",
"description": "libmodulor is a TypeScript library to create platform-agnostic applications."
}
Applications created with `libmodulor` have **6 main properties**
Strictly typed with explicit business data types
Fully typed e2e without code generation
Auto documented
Auto tested
Multi platforms/runtimes
Runnable anywhere
file: ./content/docs/concepts/architecture.mdx
meta: {
"title": "Architecture",
"description": "libmodulor follows a 4-layer architecture with UseCase, App, Product, and Target. It structures business logic efficiently, keeping apps modular and scalable."
}
`libmodulor` defines a **4-layer architecture** composed of : `UseCase`, `App`, `Product`, `Target`.
Semantically,
* a `UseCase` is part of an `App`
* an `App` is mounted in a `Product`
* a `Product` is exposed via a `Target`
* a `Target` is installed/deployed somewhere
```mermaid
graph TD;
subgraph AA [Targets]
AA1[Target 1];
AA2[Target 2];
AA3[Target 3];
end
subgraph BB [Products]
BB1[Product 1];
end
subgraph CC [Apps]
CC1[App 1]
CC2[App 2]
end
subgraph DD1 [Use Cases]
DD11[UseCase 1]
DD12[UseCase 2]
DD13[UseCase 3]
end
subgraph DD2 [Use Cases]
DD21[UseCase 4]
DD22[UseCase 5]
DD23[UseCase 6]
end
AA --> BB;
BB --> CC1;
BB --> CC2;
CC1 --> DD1;
CC2 --> DD2;
```
### UseCase
A use case is a piece of business functionality. It takes an input, processes it through lifecycle methods (`client` and/or `server`), and gives an output.
Inspired by [UML's Use case diagram](https://en.wikipedia.org/wiki/Use_case_diagram) and [Event-driven architecture](https://en.wikipedia.org/wiki/Event-driven_architecture), schematically, it could be defined as follows :
```math
O = clientMain(serverMain(I))
```
*Examples : `SignIn`, `CreatePost`, `TransferAccount`, `InviteContacts`*...
Note how it always starts with a verb.
Concretely, it's a file named `*UCD.ts` containing the definition of the use case (io, lifecycle, policies, etc.).
### App
An app is a logical group of use cases. It's like a "module" (*whatever that means*), inspired by [Domain-driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) bounded contexts.
*Examples : `Auth`, `Accounting`, `CMS`...*
Concretely, it's a folder containing metadata files (`i18n.ts`, `manifest.ts`...) and a `ucds` folder which contains the use case definitions.
### Product
A product is a logical group of apps that are assembled together. It's simply what end users know and use.
*Examples : `GitHub`, `Facebook`, `LinkedIn`, `Airbnb`...*
Concretely, it's a folder containing `i18n.ts`, `manifest.(js|ts)` and the elements corresponding to the desired targets. For example, it will contain the screens of a mobile app or the pages of a website.
### Target
A target defines how a product is "exposed" to the end user. It's a combination of platform and runtime.
*Examples : `web-react`, `web-angular`, `server-node`, `cli-node`, `cli-stricli`...*
Note that it's the only place where the "infrastructure" choices are applied. Targets can be generic and reusable across products.
file: ./content/docs/concepts/dependency-injection.mdx
meta: {
"title": "Dependency Injection (DI)",
"description": "libmodulor uses Dependency Injection to stay platform-agnostic. Apps run anywhere by injecting interfaces, avoiding platform-specific dependencies."
}
`libmodulor` relies heavily on [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) because of its **platform-agnostic** nature :
> Apps must be runnable anywhere
Therefore, most primitives are based on `injectable` classes that `inject` their dependencies.
Anywhere you see `injectable` in the upcoming guides (`UCMain`, `UCPolicy`, etc.), you are required to respect the following rule :
> Injectables must inject only interfaces and/or classes that are platform-agnostic.
For instance, you should never have `node:*`, `react-native-*` or similar imports in an app as this would break this rule.
For the same reason, even though they are called "Web Standards", you should never use them directly neither, as they are not available on all runtimes.
Instead, you should describe the behavior in an interface and create concrete implementations.
Interfaces are injected in your use cases and concrete implementations are bound in the DI container at the target level.
As an example, calling a file system is inherently platform specific.
The following interface defines the contract.
It can be safely `injected` in any `injectable`.
```typescript
interface FileManager {
readFile(): Promise;
}
// Usage
@inject('FileManager') private fileManager: FileManager;
```
On the other side, these implementations are platform-agnostic by nature.
```typescript
class NodeFileManager implements FileManager {
// Uses https://nodejs.org/api/fs.html
}
class ReactNativeFileManager implements FileManager {
// Uses https://www.npmjs.com/package/@dr.pogodin/react-native-fs
}
class BunFileManager implements FileManager {
// Uses https://bun.sh/docs/api/file-io
}
class WebFileManager implements FileManager {
// Uses nothing OR you can implement a virtual FS for web based environments
}
// Usage
container.bind('FileManager').to(NodeFileManager);
```
file: ./content/docs/concepts/philosophy.mdx
meta: {
"title": "Philosophy",
"description": "libmodulor offers flexible primitives to build apps faster, without boilerplates. Use any stack DB, frontend, server, tools & more—your choice !"
}
One might argue that, with so many "JS frameworks" on the market, there are already too many ways to build new applications today. And they would be right.
That's why the angle taken by `libmodulor` is different. Although opinionated about some aspects (see below), it is not, regarding the technical side. Instead, it focuses mainly on the "core" of your application.
Thus, you are free to use :
* the data store of your choice (PostgreSQL, MySQL, MariaDB, DynamoDB, SQLite, MongoDB...),
* the frontend framework of your choice (React, Svelte, Angular, Vue, Solid...),
* the server of your choice (Express, Fastify, Hono...),
* the meta framework of your choice (Next, Remix, Astro, Nuxt...),
* the runtime of your choice (Node, Deno, Bun...)
* the libraries of your choice (Lodash, React Query...)
* the tools of your choice (Biome, ESLint, Prettier...)
* the styling library of your choice for web (tailwind, shadcn, bootstrap, vanilla CSS...)
* the hosting of your choice (Cloud, IaaS, PaaS, On-Prem, RaspberryPi, your fridge...)
The main goal is to offer higher level primitives that make building business applications faster, without having to use a boilerplate or worse, no/low code, and thus, avoid vendor lock-in.
file: ./content/docs/examples/Basic.mdx
meta: {
"title": "Basic",
"description": "An example showcasing the mechanisms of libmodulor in one single executable file."
}
This is the perfect example to have an overview of the most important primitives of `libmodulor`.
## Get
Take the time to open the code in your favorite editor and browse it.
```sh
git clone git@github.com:c100k/libmodulor.git
cd libmodulor/examples/basic
yarn install
```
## Run
Execute the script in your Terminal Emulator.
```sh
# with node
yarn build && yarn run:node
# with bun
yarn run:bun
```
Note how it works with both Node and Bun without having to override any implementations in the container.
This is due to the high modularity of `libmodulor` and the great work the `bun` team is doing to support the Node.js API.
You get a full output of what's going on step by step.
```txt
Declaring the App
Declaring the UseCase
Declaring the Product
Declaring the Target
Initializing i18n
Initializing the UseCase
Submitting the use case empty
❌ Oops : Your email address must be filled
Filling all the fields correctly except the email (invalid)
❌ Oops : Your email address must be a valid email address
Filling a valid email
✅ Use case executed successfully
💾 Persisted record in InMemoryUCDataStore
{
aggregateId: '336fe1ee-e259-48f9-8fa4-2d3d9257947d',
appName: 'Event',
createdAt: 2025-03-07T14:25:04.756Z,
data: null,
executionMode: 'user',
id: '6296be1d-806c-4c26-b244-2d696c5b3ba5',
input: {
email: 'dexter@caramail.com',
firstname: 'Dexter',
lastname: 'Morgan'
},
name: 'Register',
organizationId: null,
userId: null
}
📓 Summary with fields from I18n/WordingManager
Your registration # : 336fe1ee-e259-48f9-8fa4-2d3d9257947d
Your email address : dexter@caramail.com
Your wonderful firstname : Dexter
Your awesome lastname : Morgan
✨ Done in 0.26s.
```
Play with the example, add features, break it 🙂.
file: ./content/docs/examples/SuperTrader.mdx
meta: {
"title": "SuperTrader",
"description": "An example showcasing a simple app to trade crypto, shares and other assets."
}
SuperTrader is a product featuring the `Trading` app with `BuyAssetUCD`, `CancelOrderUCD` and `ListOrdersUCD`, exposed on the following targets :
## UseCases
Here are the sequence diagram generated by the automatic documentation.
### BuyAsset

### CancelOrder

### ListOrders

## Get & Build
Take the time to open the code in your favorite editor and browse it.
```sh
git clone git@github.com:c100k/libmodulor.git
cd libmodulor/examples/supertrader
yarn install && touch .env && yarn build
```
## Targets
### server-node-express
The `server-node-express` target uses the built-in `node-express-server` target, is transpiled with `tsc` and executed with `node`. It uses SQLite as `UCDataStore`.
```sh
yarn run:server-node-express
```
Test in your Terminal Emulator with `curl`.
```sh
# BuyAsset
curl -X POST -H "Content-Type: application/json" http://localhost:7443/api/v1/Trading_BuyAsset
# ❌ {"message":"Invalid credentials"}
curl -X POST -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" http://localhost:7443/api/v1/Trading_BuyAsset
# ❌ {"message":"ISIN must be filled"}
curl -X POST -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" -d '{"isin":"US02079K3059","limit":123.5,"qty":150}' http://localhost:7443/api/v1/Trading_BuyAsset
# ✅ {"parts":{"_0":{"items":[{"isin":"US02079K3059","id":"882f384b-fa46-424b-8b82-e0781ab1fbec","limit":123.5,"qty":150,"status":"pending"}],"total":1}}}
# CancelOrder
curl -X DELETE -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" -d '{"id":"882f384b-fa46-424b-8b82-e0781ab1fbec"}' http://localhost:7443/api/v1/Trading_CancelOrder
# ✅ {"parts":{"_0":{"items":[{"id":"a2285506-1afd-4649-98a6-e4443d1c6ce7","isin":"US02079K3059","limit":123.5,"qty":150,"status":"cancelled"}],"total":1}}}
curl -X DELETE -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" -d '{"id":"882f384b-fa46-424b-8b82-e0781ab1fbec"}' http://localhost:7443/api/v1/Trading_CancelOrder
# ❌ {"message":"Cannot cancel an order that is not pending"}
# ListOrders
curl -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" http://localhost:7443/api/v1/Trading_ListOrders
# ✅ {"parts":{"_0":{"items":[{"id":"a2285506-1afd-4649-98a6-e4443d1c6ce7","isin":"US02079K3059","limit":123.5,"qty":150,"status":"cancelled"}],"total":1}}}
```
Keep your server running to test the "client" targets defined below.
### web
The `web` target uses the built-in `react-web-pure` target, is bundled with `vite` and uses custom components made with `tailwindcss@v4` & `daisyUI@v5`.
It is served as a SPA by the server defined above.
Test in your browser at [http://localhost:7443](http://localhost:7443).

### cli
The `cli` target uses the built-in `node-core-cli` target, is transpiled with `tsc` and executed with `node`.
Test in your Terminal Emulator.
```sh
# BuyAsset
yarn run:cli Trading_BuyAsset
# ❌ ISIN must be filled
yarn run:cli Trading_BuyAsset --isin US02079K3059 --limit 123.5 --qty 150
# ✅ {"parts":{"_0":{"items":[{"isin":"US02079K3059","id":"c1ef95f1-f6e1-4616-a7a5-5584758b3a65","limit":123.5,"qty":150,"status":"pending"}],"total":1}}}
# CancelOrder
yarn run:cli Trading_CancelOrder --id c1ef95f1-f6e1-4616-a7a5-5584758b3a65
# Are you sure ? [Y,y = Yes / N,n = Cancel] : y
# ✅ {"parts":{"_0":{"items":[{"id":"c1ef95f1-f6e1-4616-a7a5-5584758b3a65","isin":"US02079K3059","limit":123.5,"qty":150,"status":"cancelled"}],"total":1}}}
yarn run:cli Trading_CancelOrder --id c1ef95f1-f6e1-4616-a7a5-5584758b3a65
# Are you sure ? [Y,y = Yes / N,n = Cancel] : y
# ❌ Cannot cancel an order that is not pending
# ListOrders
yarn run:cli Trading_ListOrders
# ✅ {"parts":{"_0":{"items":[{"id":"a2285506-1afd-4649-98a6-e4443d1c6ce7","isin":"US02079K3059","limit":123.5,"qty":150,"status":"cancelled"},{"id":"882f384b-fa46-424b-8b82-e0781ab1fbec","isin":"US02079K3059","limit":123.5,"qty":150,"status":"pending"},{"id":"c1ef95f1-f6e1-4616-a7a5-5584758b3a65","isin":"US02079K3059","limit":123.5,"qty":150,"status":"cancelled"},{"id":"78509b91-0f33-43a2-b6e3-2ced5a1fa171","isin":"US02079K3059","limit":123.5,"qty":150,"status":"pending"}],"total":4}}}
```
### mcp-server
The `mcp-server` target uses the built-in `node-mcp-server` target, is transpiled with `tsc` and exposes an `index.js` file to be used by the MCP client.
Test in [Claude Desktop](https://claude.ai/download).
Register the MCP server by copy/pasting the following config, adapting the absolute path with `pwd`.
```sh
nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
```json
{
"mcpServers": {
"libmodulor-supertrader": {
"command": "node",
"args": [
"/ABSOLUTE_PATH_TO_THE_CWD/dist/products/SuperTrader/mcp-server/index.js"
]
}
}
}
```
Launch Claude Desktop.
At the bottom right of the prompt you should see a little hammer 🔨 indicating `1 MCP Tool available`.
Click on it. You should see the `Trading_BuyAsset` use case registered.
Write a prompt to buy an asset.
```txt
Dear Claude. Please buy 150 shares of Google.
```

### rn
The `rn` target uses the built-in `react-native-pure` target, is bundled with `expo` so it can run on `android` and `ios`.
Make sure your [environment is setup](https://reactnative.dev/docs/environment-setup) to be able to use the Android Emulator and iOS Simulator (macOS only).
```sh
yarn run:rn:android
yarn run:rn:ios
```

### server-node-hono
The `server-node-hono` is the same as `server-node-express` defined above, except that it's using `hono` instead of `express`.
```sh
yarn run:server-node-hono
```
You can go back to all the client targets and run them. It will work the exact same way.
### server-nextjs
The `server-nextjs` target uses the built-in `server-nextjs` target and the same components as the `web` target defined above.
```sh
yarn run:server-nextjs
```
Test in your browser at [http://localhost:3000](http://localhost:3000).
## Inspect UCDataStore
The `server-*` targets rebinds the `UCDataStore` from in memory (default) to `KnexUCDataStore` configured for SQLite.
Therefore, if you have executed the use case via the targets above, you can see the data in the DB file.
```sh
open dist/products/SuperTrader/server-node-express/uc-data-store.sqlite
open dist/products/SuperTrader/server-node-hono/uc-data-store.sqlite
open dist/products/SuperTrader/server-nextjs/uc-data-store.sqlite
```

file: ./content/docs/guides/create-app.mdx
meta: {
"title": "Create an app",
"description": "An app is a logical group of use cases. It's like a \"module\" (whatever that means), inspired by Domain-driven design (DDD) bounded contexts."
}
Its logical representation is a directory named `src/apps/{AppName}` that has the following structure :
```sh
📁 src
📁 lib
📁 ucds
📄 i18n.ts
📄 manifest.ts
📁 test
📄 index.ts
```
## src => Source
### lib
This folder contains [data types](./create-data-type), [policies](./create-policy), types, interfaces, classes, functions, consts... that are used across one or multiple use cases.
You are free to organize this folder the way you want.
### ucds => use case definitions
This folder contains the use case definitions of the app. Check [Create a use case](./create-use-case) for more details.
### manifest
It's a file named `manifest.ts` exporting a const named `Manifest` that must satisfy the `AppManifest` interface.
```typescript title="src/manifest.ts"
import type { AppManifest } from 'libmodulor';
export const Manifest = {
languageCodes: ['en'],
name: 'Trading',
ucReg: {},
} satisfies AppManifest;
```
`languageCodes` lists all the [country codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) that the app supports.
For any language listed here, `i18n` (see below), must contain the corresponding entry.
`name` must be the same as the app directory name.
`ucReg` stands for "use case registry". It lists all the use cases of the app. Check out [Create a use case](./create-use-case) for more details.
### i18n
It's a file named `i18n.ts` exporting a const named `I18n` that must satisfy the `AppI18n` interface.
```typescript title="src/i18n.ts"
import type { AppI18n } from 'libmodulor';
export const I18n: AppI18n = {
en: {},
};
```
It contains translations for the common things that can be translated in the app. Check out [Translate an app](./translate-app) for more details.
You can leave translations empty and things will still work.
In this case, the library will `humanize` the fields.
Although it can work in some cases, it's not ideal in terms of UX.
## test
Check [Test an app](./test-app) for more details.
## index
It's a barrel file exporting only what's consumed outside the app (typically `i18n`, `manifest` and some use case defs).
```typescript title="index.ts"
// Expose only what's necessary
export { I18n } from './src/i18n.js';
export { Manifest } from './src/manifest.js';
```
file: ./content/docs/guides/create-data-type.mdx
meta: {
"title": "Create a data type",
"description": "Use cases define input/output using specific data types."
}
## Intro
A data type is a class that extends the `TBase` class directly or indirectly.
`libmodulor` provides [data types](../references/data-types) that are commonly used.
But some apps need specific data types.
## Example
As an example, defining an [ISIN](https://www.isin.org) as a `string` is too approximative.
Indeed, it has a specific length, format, etc.
```typescript title="src/lib/TISIN.ts"
export type ISIN = Capitalize;
export class TISIN extends TString {
public static readonly FORMAT: RegExp = /^[A-Z]{2}[A-Z0-9]{9}[0-9]$/;
constructor(constraints?: TStringConstraints) {
super({
...constraints,
format: { f: 'ISIN', regexp: TISIN.FORMAT },
});
}
public override tName(): TName {
return 'ISIN';
}
public override example(): ISIN {
return 'US02079K3059';
}
}
```
It defines two things that need to be exported :
* A type (e.g. `ISIN` that specifies the data type with a TypeScript type)
* A class (e.g. `TISIN` that specifies how the data type behaves)
`libmodulor` provides [base data types](https://github.com/c100k/libmodulor/tree/master/dist/esm/dt/base) that you can extend.
## i18n
In the example above we've defined a `format` for the ISIN.
When the end user inputs an invalid ISIN, it will trigger an error with the following key: `validation_format_ISIN`.
This key can be translated in the app's `i18n`. Check [Translate an app](./translate-app) for more details.
file: ./content/docs/guides/create-policy.mdx
meta: {
"title": "Create a policy",
"description": "A policy defines whether a use case can be accessed/executed or not."
}
## Intro
A policy is a class that implements the `UCPolicy` interface.
`libmodulor` provides [policies](../references/policies) that are commonly used.
But some apps need specific policies.
## Example
As an example, you can define a policy to allow some use cases only to "Pro" users.
```typescript title="src/lib/ProUCPolicy.ts"
@injectable()
export class ProUCPolicy<
I extends UCInput | undefined = undefined,
OPI0 extends UCOPIBase | undefined = undefined,
OPI1 extends UCOPIBase | undefined = undefined,
> implements UCPolicy
{
constructor(
@inject('UserDataStore') private userDataStore: UserDataStore,
) {}
public async canBeExecutedPreAuth(): Promise {
return false;
}
public async exec({
uc,
}: UCPolicyInput): Promise {
const out = defaultUCPolicyOutput();
const user = await this.userDataStore.get(uc.auth.user.id);
if (!user) {
return out;
}
out.allowed = user.plan === 'Pro';
return out;
}
}
```
In this example, we get the information from a hypothetical `UserDataStore` but you are free to inject anything you need in here.
file: ./content/docs/guides/create-product.mdx
meta: {
"title": "Create a product",
"description": "A product is a logical group of apps that are assembled together. It's simply what end users know and use."
}
Its logical representation is a directory named `src/products/{AppName}` that has the following structure :
```sh
📁 target1
📁 ...
📁 targetn
📄 i18n.ts
📄 manifest.ts
```
## target1 .. targetn
Each directory corresponds to a target exposing the product.
If the product is supported by a webapp and a mobile app, there will typically be `server`, `web`, `rn` directories (or similar).
Check [Expose a target](./expose-target) for more details.
## manifest
It's a file named `manifest.ts` exporting a const named `Manifest` that must satisfy the `ProductManifest` interface.
```typescript title="manifest.ts"
export const Manifest: ProductManifest = {
appReg: [{ name: 'Trading' }],
name: 'SuperTrader',
};
```
`appReg` stands for "app registry". It lists all the apps the product uses.
It provides an optional `ucds.exclude` property in order to prevent some use cases from being mounted.
This is particularly useful when you have a generic app (e.g. `Auth`) providing a large range of use cases,
but some products don't need all of them.
`name` must be the same as the product directory name.
## i18n
It's a file named `i18n.ts` exporting a const named `I18n` that must satisfy the `ProductI18n` interface.
Unlike the app `i18n`, this one gives you more freedom to define translation keys.
```typescript title="i18n.ts"
import { I18nEN } from 'libmodulor/locales/en';
import { I18n as TradingI18n } from '../../apps/Trading/index.js';
export const I18n: ProductI18n = {
en: {
...I18nEN,
...TradingI18n.en,
p_desc: 'A simple app to trade crypto, shares and other assets',
p_slogan: 'Trading made simple',
},
};
```
Note how we include the `i18n` of :
* the library (`I18nEN`) to provide basic translations of exposed primitives
* the apps (`TradingI18n`) to provide apps translations
file: ./content/docs/guides/create-project.mdx
meta: {
"title": "Create a project",
"description": "libmodulor can be used in a brownfield project but we recommend starting a greenfield project to enjoy the full expressiveness and convention system."
}
## With npx
```sh
npx libmodulor CreateProject --projectName my-super-project
```
By default, it will create a folder named `my-super-project` in the current working directory and install the dependencies using `yarn`.
If you want to change the defaults (initial commit, pkg manager, etc.), run the `--help` command.
```sh
npx libmodulor --help | grep CreateProject -C 10
```
Execution should take a few seconds and give the following output.
```sh
Need to install the following packages:
libmodulor@latest
Ok to proceed? (y) y
2025-02-28T15:32:09.446Z [info] Creating root dir : my-super-project
2025-02-28T15:32:09.447Z [info] Initializing git repository
2025-02-28T15:32:09.495Z [info] Creating config files
2025-02-28T15:32:09.496Z [info] Creating apps and products directories
2025-02-28T15:32:09.496Z [info] Installing dependencies
2025-02-28T15:32:16.339Z [info] Committing
2025-02-28T15:32:16.483Z [info] Testing dev command : yarn lint
2025-02-28T15:32:17.134Z [info] Testing dev command : yarn test
2025-02-28T15:32:18.342Z [info] Done ! Project ready ! ✅ 🚀
```
The generated directory has the following structure :
```sh
📁 src
📁 apps # the apps of the project (empty for now)
📁 products # the products of the project (empty for now)
📄 .gitignore # includes the files/dirs patterns to exclude from version control
📄 biome.json # config for the linter (feel free to change it if you don't like the defaults and run yarn lint again)
📄 package.json # main config file containing info, scripts and dependencies
📄 README.md # main documentation file (feel free to enhance it with explanations about the purpose of your project)
📄 tsconfig.json # config file for TypeScript (pretty strict by default)
📄 vitest.config.ts # config file for Vitest, the test runner
```
Optionally, you can create a remote repository (e.g. on GitHub) and push it.
## Manually
If you don't like the magic behind `npx`, or if you want to add `libmodulor` to an existing project, you can install it manually.
Check the required dependencies in [package.json](https://github.com/c100k/libmodulor/blob/master/dist/esm/apps/Helper/src/lib/project.js#L29).
file: ./content/docs/guides/create-target.mdx
meta: {
"title": "Create a target",
"description": "A target defines how a product is \"exposed\" to the end user. It's a combination of platform and runtime."
}
## Intro
`libmodulor` provides [targets](../references/targets) that are commonly used.
But depending on your needs, you might want to create your own targets as there is a high probability that the provided ones do not fit your criteria, especially the [GUI](https://en.wikipedia.org/wiki/Graphical_user_interface) ones since they are very basic.
Yet, they are good examples to take inspiration from in order to understand how apps, use cases and products are instrumented.
## CLI
A CLI target exposes a product so it can be used in a [Terminal Emulator](https://en.wikipedia.org/wiki/List_of_terminal_emulators) :
```sh
node index.js SignIn --email dexter@caramail.com
> Password : *******************
node index.js --help
node index.js --version
```
To create a CLI target, you need to implement the `CLIManager` interface.
See [node-core-cli](https://github.com/c100k/libmodulor/tree/master/dist/esm/target/node-core-cli/NodeCoreCLIManager.js) as an example.
Important aspects to take into account (non-exhaustive list) :
* the command must be mounted at `uc.def.ext.cmd.mountAt ?? ucMountingPoint(uc)`
* the command must execute `uc.def.lifecycle.client.main` according to the `policy`
* each input field must be mapped to a dedicated flag (e.g. `email => --email`)
* handle the fields type when parsing the command (e.g. `number`, `boolean`)
* handle the field cardinality when parsing the command (e.g. `--tag Work --tag Boss` => `tag: ['Work', 'Boss']`)
* handle the field "sensitivity" (see `isSensitive()`) by prompting for them instead of providing a flag to avoid leaking secrets in your history
* expose common commands usually provided by a CLI program (e.g. `--help`, `--version`, etc.) (these are usually not use cases)
* build the help based on `uc.def.io` and `WordingManager`
* build the version based on your `package.json`
* handle unknown commands by whether failing or displaying the help section
Here is a great list of [solutions](https://bloomberg.github.io/stricli/docs/getting-started/alternatives) to build your own.
## Server
A server target exposes a product so it can be called as a REST-like API :
```sh
curl \
-X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" \
-d '{"email":"dexter@caramail.com","password":"thedarkpassenger305"}' \
http://localhost:7443/api/v1/SignIn
```
To create a server target, you need to implement the `ServerManager` interface.
See [node-express-server](https://github.com/c100k/libmodulor/tree/master/dist/esm/target/node-express-server/NodeExpressServerManager.js) as an example.
Important aspects to take into account (non-exhaustive list) :
* the use case must be mounted at the `path` and `pathAliases` provided by [ucHTTPContract](https://github.com/c100k/libmodulor/tree/master/dist/esm/uc/utils/ucHTTPContract.js)
* the request handler must execute `uc.def.lifecycle.server.main` according to the `policy`
* the request handler must handle the `uc.def.io.o.sideEffects`
* the request handler must return a `204` when there is no `uc.def.io.o`
* the request handler must handle the output transformation based on `ext.http.transform`
* the appropriate middleware must be mounted to handle `uc.def.sec.publicApiKeyCheckType`
* the appropriate middleware must be mounted to handle `uc.def.sec.authType`
* cookies must be implemented to handle authentication
This is the implementation used in the apps automated tests. Check [Test an app](./test-app) for more details.
## GUI
A GUI target exposes a product as a user interface.
See [react-native-pure](https://github.com/c100k/libmodulor/tree/master/dist/esm/target/react-native-pure) and [react-web-pure](https://github.com/c100k/libmodulor/tree/master/dist/esm/target/react-web-pure) as examples.
Typically, it provides "components" to :
* access a use case (e.g. a button or a link)
* interact with a use case (e.g. a button or a form)
Important aspects to take into account (non-exhaustive list) :
* the "access" component must handle the `uc.def.lifecycle.client.policy`
* the "interact" component must display a form when the use case needs input filling
* the form fields must handle the input fields type, cardinality, etc.
* the "interact" component must display a control (e.g. a button) when the use case does not need input filling
* the button submitting a use case must handle the different statuses (`idle`, `changing`, `submitting`, etc.)
* the wording must rely on `WordingManager`
## MCP Server
A MCP server target exposes a product as a MCP server.
See [node-mcp-server](https://github.com/c100k/libmodulor/tree/master/dist/esm/target/node-mcp-server) as an example.
Like a regular server, it exposes the use cases as MCP tools.
## Others
The list above corresponds to the current state of human interfaces in Software. We can bet that new ones will arrive pretty soon. So the only limit is your imagination.
We're looking forward to working on AR/VR targets, Voice targets, Haptic targets, etc...
file: ./content/docs/guides/create-use-case.mdx
meta: {
"title": "Create a use case",
"description": "A use case is a piece of business functionality. It takes an input, processes it through lifecycle methods (client and/or server), and gives an output."
}
In this documentation and in the code, use case is often refered to as `UC` or `uc`.
Since it's a notion widely used in `libmodulor`, the abbreviation makes sense to make things more concise.
Thus, `UCD` stands for `Use Case Definition`, `UCIF` stands for `Use Case Input Field` and so on.
## Metadata
A use case is declared by defining its metadata in the app's `manifest` as seen in [Create an app](./create-app).
```typescript title="src/manifest.ts"
ucReg: {
BuyAsset: {
action: 'Create',
beta: true, // Optional
icon: 'plus',
name: 'BuyAsset',
new: true, // Optional
sensitive: true, // Optional
},
},
```
The metadata defines several properties that are used by the app, products and targets to instrument the use case.
The `action` can be used by a server target to decide which HTTP verb to use.
The `icon` can be used by a GUI to display it alongside the label in a button.
You are free to set the icon of your choice, corresponding to the icon library you're using in your GUI target (e.g. FontAwesome).
The `beta` and `new` flags can be used by a GUI target to display a badge next to the button giving access to the use case, in order to show the user that it's a new feature.
The `sensitive` flag can be used to ask the user to confirm when they execute a use case (e.g. a destructive use case).
## UCD => Use Case Definition
A use case is defined in a file named `src/apps/{AppName}/src/ucds/{UCName}UCD.ts`.
It must export :
* the interface corresponding to the input (if any)
* the interface corresponding to the OPIs (if any)
* a const named `{UCName}UCD` that satisfies the `UCDef` interface.
### IO => input/output
A use case can define 0 or 1 input and 0, 1 or 2 output part items (OPI). Based on this, the definition of a use case has one of the following types :
```typescript
UCDef // 0 input, 0 OPI
UCDef // 1 input, 0 OPI
UCDef // 1 input, 1 OPI
UCDef // 0 input, 1 OPI
UCDef // 1 input, 2 OPIs
```
#### I => input
It is an interface that must extend the `UCInput` interface directly or indirectly.
```typescript title="src/ucds/BuyAssetUCD.ts"
export interface BuyAssetInput extends UCInput {
isin: UCInputFieldValue;
limit: UCInputFieldValue;
qty: UCInputFieldValue;
}
```
It must contain only properties of type `UCInputFieldValue`.
You can use one of the [existing data types](../references/data-types) or [create a data type](./create-data-type).
The same fields must be declared in `UCDef.io.i.fields`.
```typescript title="src/ucds/BuyAssetUCD.ts"
fields: {
isin: {
type: new TISIN(),
},
limit: {
type: new TAmount('USD'),
},
qty: {
type: new TUIntQuantity(),
},
},
```
This definition might seem redundant but it offers more expressiveness to define advanced rules for data types than the simple TypeScript type system.
#### O => output
It is an interface that must extend the `UCOPIBase` interface directly or indirectly.
```typescript title="src/ucds/BuyAssetUCD.ts"
export interface BuyAssetOPI0 extends UCOPIBase {
executedDirectly: UCOPIVal;
}
```
The same fields must be declared in `UCDef.io.o.parts._0`.
```typescript title="src/ucds/BuyAssetUCD.ts"
fields: {
executedDirectly: {
type: new TBoolean(),
},
},
```
### Lifecycle
A use case can define a `client` and/or a `server` lifecycle.
#### client
It is an object that satisfies the `UCClientDef` interface.
```typescript title="src/ucds/BuyAssetUCD.ts"
client: {
main: BuyAssetClientMain,
policy: EverybodyUCPolicy,
},
```
The `main` property references a class that implements the `UCMain` interface.
This class can be defined just above the `UCDef` like the following :
```typescript title="src/ucds/BuyAssetUCD.ts"
@injectable()
class BuyAssetClientMain implements UCMain {
constructor(
@inject('UCTransporter')
private ucTransporter: UCTransporter,
) {}
public async exec({
uc,
}: UCMainInput): Promise<
UCOutputOrNothing
> {
return this.ucTransporter.send(uc);
}
}
```
In this class, you can inject whatever you want and define all your client side business logic in `exec`.
In this specific case, we are simply sending the use case to the server.
The `policy` defines whether the current user can see and/or execute the use case.
You can use one of the [existing policies](../references/policies) or [create a policy](./create-policy).
#### server
It is an object that satisfies the `UCServerDef` interface.
```typescript title="src/ucds/BuyAssetUCD.ts"
server: {
execMode: UCExecMode.USER, // Optional
init: BuyAssetServerInit, // Optional
main: BuyAssetServerMain,
policy: EverybodyUCPolicy,
},
```
The `main` property references a class that implements the `UCMain` interface.
This class must be defined in a dedicated file `src/apps/{AppName}/src/ucds/{UCName}ServerMain.ts` like the following :
```typescript title="src/ucds/BuyAssetServerMain.ts"
@injectable()
export class BuyAssetServerMain implements UCMain {
constructor(@inject('UCManager') private ucManager: UCManager) {}
public async exec({
uc,
}: UCMainInput): Promise<
UCOutput
> {
// >=> Persist the order
const { aggregateId } = await this.ucManager.persist(uc);
// >=> TODO : Check the user has enough funds to place the order
// >=> TODO : Send the order to a queue for processing
const executedDirectly: BuyAssetOPI0['executedDirectly'] = false;
return new UCOutputBuilder()
.add({
executedDirectly,
id: aggregateId,
})
.get();
}
}
```
Using a comment starting with `// >=> ` in `ClientMain` and `ServerMain` has a specific meaning as we'll see in [Test the app](./test-app).
The `init` property references a class that implements the `UCInit` interface.
It allows you to setup stuff when the use case is mounted (e.g. creating some caches or data stores when mounting the use case on a server target).
The `policy` defines whether the current user can execute the use case.
You can use one of the [existing policies](../references/policies) or [create a policy](./create-policy).
### Metadata
As seen above, the metadata is declared in the app manifest.
In the UCD, you simply need to reference this declaration.
```typescript title="src/ucds/BuyAssetUCD.ts"
metadata: Manifest.ucReg.BuyAsset,
```
## Full definition
All in all, a typical use case definition looks like this :
```typescript title="src/ucds/BuyAssetUCD.ts"
export interface BuyAssetInput extends UCInput {
isin: UCInputFieldValue;
limit: UCInputFieldValue;
qty: UCInputFieldValue;
}
export interface BuyAssetOPI0 extends AggregateOPI0 {
executedDirectly: UCOPIVal;
}
@injectable()
class BuyAssetClientMain implements UCMain {
constructor(
@inject('UCTransporter')
private ucTransporter: UCTransporter,
) {}
public async exec({
uc,
}: UCMainInput): Promise<
UCOutputOrNothing
> {
return this.ucTransporter.send(uc);
}
}
export const BuyAssetUCD: UCDef = {
io: {
i: {
fields: {
isin: {
type: new TISIN(),
},
limit: {
type: new TAmount('USD'),
},
qty: {
type: new TUIntQuantity(),
},
},
},
o: {
parts: {
_0: {
fields: {
executedDirectly: {
type: new TBoolean(),
},
},
},
},
},
},
lifecycle: {
client: {
main: BuyAssetClientMain,
policy: EverybodyUCPolicy,
},
server: {
main: BuyAssetServerMain,
policy: EverybodyUCPolicy,
},
},
metadata: Manifest.ucReg.BuyAsset,
};
```
## Advanced
The `UCDef` interface allows to define much more properties like `ext` and `sec` or `sideEffects`. These are more advanced topics and will be addressed in dedicated guides.
file: ./content/docs/guides/expose-target.mdx
meta: {
"title": "Expose a target",
"description": "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 :
```sh
📄 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.
```typescript title="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](https://github.com/c100k/libmodulor/blob/master/dist/esm/utils/ioc/bindNodeCore.js)
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.).
```typescript title="container.ts"
bindCommon(container, settings);
bindNodeCore(container); // [!code ++]
```
### bindNodeCLI
[bindNodeCLI](https://github.com/c100k/libmodulor/blob/master/dist/esm/utils/ioc/bindNodeCLI.js)
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`.
```typescript title="container.ts"
bindCommon(container, settings);
bindNodeCLI(container); // [!code ++]
```
### bindRN
[bindRN](https://github.com/c100k/libmodulor/blob/master/dist/esm/utils/ioc/bindRN.js)
binds all the standard interfaces used internally by `libmodulor` to React Native implementations.
To be used in any target running on React Native.
```typescript title="container.ts"
bindCommon(container, settings);
bindRN(container); // [!code ++]
```
### bindServer
[bindServer](https://github.com/c100k/libmodulor/blob/master/dist/esm/utils/ioc/bindServer.js)
binds all the standard interfaces used internally by `libmodulor` `NodeExpressServerManager` to basic implementations.
```typescript title="container.ts"
bindCommon(container, settings);
bindServer(container); // [!code ++]
```
### bindWeb
[bindWeb](https://github.com/c100k/libmodulor/blob/master/dist/esm/utils/ioc/bindWeb.js)
binds all the standard interfaces used internally by `libmodulor` to web implementations.
```typescript title="container.ts"
bindCommon(container, settings);
bindWeb(container); // [!code ++]
```
### 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.
```typescript title="container.ts"
container.bind('LLMManager').to(MistralAILLMManager);
```
You can also override the bindings done previously by using `rebind`.
```typescript title="container.ts"
container.rebind('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.
```typescript title="server/index.ts"
import container from './container.js';
await container.get('I18nManager').init();
await container.resolve(ServerBooter).exec({});
```
```tsx title="web-react/index.tsx"
import App from './components/App.js';
import container from './container.js';
ReactDOM.createRoot(rootElt).render(
,
);
```
```typescript title="cli/index.ts"
import container from './container.js';
await container.get('I18nManager').init();
await container.resolve(NodeCoreCLIManager).exec({});
```
```typescript title="mcp-server/index.ts"
import container from './container.js';
await container.get('I18nManager').init();
await container.resolve(MCPServerBooter).exec({});
```
```tsx title="rn/index.tsx"
import App from './components/App.js';
import container from './container.js';
function Index(): ReactElement {
return (
);
}
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 `web` 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.
```typescript title="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.
```typescript title="server/settings.ts"
export type S = LoggerLevel & OpenAILLMManagerSettings & ServerManagerSettings; // & ...
export const settings: SettingsFunc = (_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.
```typescript title="server/container.ts"
container.rebind('SettingsManager').to(EnvSettingsManager);
```
Change the value of the `oai_api_key` setting.
```typescript title="server/settings.ts"
oai_api_key: 'MyAPIKey', // [!code --]
oai_api_key: SettingsManagerMandatoryPlaceholder, // [!code ++]
```
Introduce a `.env` file and add it to `.gitignore`.
```properties title="server/.env"
app_oai_api_key=MySuperSecretAPIKey
```
Finally, when you start your server, provide this env file.
```sh
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](https://www.vaultproject.io) instead of environment variables.
Again, `libmodulor` provides basics, but you can extend it the way you want.
file: ./content/docs/guides/test-app.mdx
meta: {
"title": "Test an app",
"description": "libmodulor provides a full mechanism to automatically test the use cases of an app."
}
`libmodulor` relies on [vitest](https://vitest.dev) to run the tests and [@vitest/coverage-v8](https://vitest.dev/guide/coverage) for the coverage.
## Configurator
By default, no tests are executed for an app. First, create a file named `src/apps/{AppName}/test/Configurator.ts`.
```typescript title="test/Configurator.ts"
@injectable()
export class Configurator extends SimpleAppTesterConfigurator {}
```
Generate the automated tests for the app.
```sh
yarn libmodulor GenerateAppsTests
```
Run the tests.
```sh
yarn libmodulor TestApp --appName {AppName}
```
```sh
% Coverage report from v8
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 98.83 | 80 | 80 | 98.83 |
Trading | 0 | 0 | 0 | 0 |
index.ts | 0 | 0 | 0 | 0 | 1
Trading/src | 100 | 100 | 100 | 100 |
i18n.ts | 100 | 100 | 100 | 100 |
manifest.ts | 100 | 100 | 100 | 100 |
Trading/src/ucds | 100 | 100 | 100 | 100 |
BuyAssetServerMain.ts | 100 | 100 | 100 | 100 |
BuyAssetUCD.ts | 100 | 100 | 100 | 100 |
------------------------|---------|----------|---------|---------|-------------------
2024-12-29T11:00:53.178Z [info] Coverage Report => open src/apps/Trading/test/reports/coverage/index.html
2024-12-29T11:00:53.178Z [info] Simple HTML Report => open src/apps/Trading/test/reports/simple-html/index.html
```
In addition to running the tests, it also :
* Generates a coverage reports : `src/apps/{AppName}/test/reports/coverage/index.html`
* Generates a test scenarios report : `src/apps/{AppName}/test/reports/simple-html/index.html`
* Update the app's README : `src/apps/{AppName}/README.md`
## Coverage report
This is a regular coverage report generated by `@vitest/coverage-v8`.
## Test scenarios report
This is generated by `libmodulor` providing you an HTML file containing table listing all the scenarios tested.
## README.md
This is generated by `libmodulor` providing an overview of the app with :
* a sequence diagram for each use case
* a technical summary for all the use cases
It gives a great overview of what the app does.
In [Create a use case](./create-use-case), we mentioned comments starting with `// >=> `.
This is where they enter in action. These comments are displayed in an awesome way in the sequence diragram,
giving an overview on what's going on in the "use case box".
## Customize
The `Configurator` class offers a basic setup.
But in most advanced apps, it needs to be customized to handle more complex scenarios.
It offers methods that can be overriden to achieve this goal.
### Auth setters
By default, each use case is tested for multiple auth settings (anonymous, authenticated, etc.).
You can override this behavior by providing your own.
### Implementations
Some apps need specific bindings. This is the case when they have interfaces/implementations defined in the `lib` folder, that are used by use cases.
You can bind those implementations and also rebind some default bindings.
### Clearing
Some use cases create side effects that can impact others.
You can define the logic to clear these side effects between each test execution.
### Flows
Use cases are automatically tested in isolation. But very often we want to test them in a real life scenario.
You can create flows to test a list of use cases one after the other to simulate a real user behavior.
For instance, for an `Auth` app, you can test the following flow : `SignUp` > `SignIn` > `SignOut`.
### Input fillers
Use cases are tested with data from their data types examples and bad data.
You can define other inputs for specific use cases to see how they behave.
### Options
Override the default options used by the test runner.
### Seed
Some apps need data that has been pre-populated in some data store.
You can `seed` these data stores.
### Side effects
Some use cases perform side effects that we want to monitor. For instance, some of them can send emails.
You can register these side effects and they will be asserted and displayed in reports.
### Specific assertions
By default, the tests are asserted via snapshots. But sometimes, use cases return different data at each execution (e.g. the "current" timestamp).
Although we recommended making the tests deterministic by using `Fake*` implementations, sometimes it's not possible or not acceptable.
In this case, you can override the snapshot assertion by a specific one using the `hash` of the test execution to identify it.
file: ./content/docs/guides/translate-app.mdx
meta: {
"title": "Translate an app",
"description": "Apps can be translated in multiple languages."
}
The translation keys must follow a specific pattern. Each type of translation has their own pattern.
## dt => data type
Translate the `options` (i.e. the possible values) of a specific data type.
```typescript title="src/i18n.ts"
en: {
dt_TYesNo_no_desc: 'No',
dt_TYesNo_no_label: 'You disagree',
dt_TYesNo_yes_desc: 'Yes',
dt_TYesNo_yes_label: 'You agree',
},
fr: {
dt_TYesNo_no_desc: 'Non',
dt_TYesNo_no_label: "Pas d'accord !",
dt_TYesNo_yes_desc: 'Oui',
dt_TYesNo_yes_label: "Je suis d'accord !",
}
```
## err => errors
Translate specific errors thrown in the app specific code present in lib, ucds, policies, etc.
```typescript title="src/i18n.ts"
en: {
err_account_disabled: 'Your account has been disabled',
},
fr: {
err_account_disabled: 'Votre compte a été désactivé',
}
```
## uc => use case
Translate the various values associated to a use case.
```typescript title="src/i18n.ts"
en: {
uc_DeleteAccount_client_confirm_cancel: "I'm staying",
uc_DeleteAccount_client_confirm_confirm: "I'm out",
uc_DeleteAccount_client_confirm_message: "You won't be able to recover your account",
uc_DeleteAccount_client_confirm_title: "Delete your account?",
uc_ListIssues_op_0_empty: '',
uc_ListIssues_op_0_label: 'Issues summary',
uc_ListIssues_op_1_empty: 'No issues',
uc_ListIssues_op_1_label: 'Issues',
uc_SignUp_desc: "Sign up",
uc_SignUp_label: "Create my account now",
uc_SignUp_i_submit_changing: 'Initializing',
uc_SignUp_i_submit_idle: 'Sign up',
uc_SignUp_i_submit_initializing: 'Initializing',
uc_SignUp_i_submit_submitting: 'Signing up',
},
fr: {
uc_DeleteAccount_client_confirm_cancel: "Je reste",
uc_DeleteAccount_client_confirm_confirm: "Je m'en vais",
uc_DeleteAccount_client_confirm_message: "Cette opération est irréversible",
uc_DeleteAccount_client_confirm_title: "Supprimer votre compte ?",
uc_ListIssues_op_0_empty: '',
uc_ListIssues_op_0_label: "Résumé des problèmes",
uc_ListIssues_op_1_empty: 'Aucun problème',
uc_ListIssues_op_1_label: 'Problèmes',
uc_SignUp_desc: "S'inscrire",
uc_SignUp_label: "Créer mon compte maintenant",
uc_SignUp_i_submit_changing: 'Initialisation',
uc_SignUp_i_submit_idle: "S'inscrire",
uc_SignUp_i_submit_initializing: 'Initialisation',
uc_SignUp_i_submit_submitting: 'Inscription',
},
```
## ucif => use case input fields
Translate the various values associated to a use case input field.
```typescript title="src/i18n.ts"
en: {
ucif_email_desc: "Your professional email address",
ucif_email_label: "Email address",
},
fr: {
ucif_email_desc: "Votre adresse email professionnelle",
ucif_email_label: "Adresse email",
},
```
## ucof => use case output fields
Translate the various values associated to a use case output field.
```typescript title="src/i18n.ts"
en: {
ucof_balance_desc: "Account balance computed from transactions",
ucof_balance_label: "Balance",
},
fr: {
ucof_balance_desc: "Solde du compte calculé à partir des opérations",
ucof_balance_label: "Solde",
},
```
## validation
Translate the validation messages of specific data types or custom validation rules.
```typescript title="src/i18n.ts"
en: {
validation_format_ZipCode: 'Must be a valid zipcode with 5 digits',
},
fr: {
validation_format_ZipCode: 'Doit être un code postal valide à 5 chiffres',
}
```
file: ./content/docs/references/data-types.mdx
meta: {
"title": "Data Types",
"description": "libmodulor provides data types that are commonly used in apps."
}
## Base
[Base data types](https://github.com/c100k/libmodulor/tree/master/dist/esm/dt/base) are "generic".
Except in some cases, we don't recommend using them directly in use cases as they lack semantic meaning.
Instead, you should extend them to create "final" data types.
| # | Name | HTML Input Type | RN Input Mode | Example | Formatted |
| --- | --------- | --------------- | ------------- | ------- | --------- |
| `0` | `Boolean` | `checkbox` | `text` | `true` | `✔️` |
| `1` | `Int` | `number` | `numeric` | `1` | `1` |
| `2` | `Number` | `number` | `decimal` | `1` | `1` |
| `3` | `Object` | `text` | `text` | `{}` | `{}` |
| `4` | `String` | `text` | `text` | `Miami` | `Miami` |
| `5` | `UInt` | `number` | `numeric` | `1` | `1` |
## Final
[Final data types](https://github.com/c100k/libmodulor/tree/master/dist/esm/dt/final) represent real life notions.
They extend directly or indirectly base data types.
This list is naturally not exhaustive and data types are regularly created to fit new use cases.
| # | Name | HTML Input Type | RN Input Mode | Example | Formatted |
| ---- | ---------------------- | --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0` | `Address` | `text` | `text` | `55 Rue du Faubourg Saint-Honoré, 75008 Paris` | `55 Rue du Faubourg Saint-Honoré, 75008 Paris` |
| `1` | `Amount` | `number` | `decimal` | `999.99` | `€999.99` |
| `2` | `ApiKey` | `password` | `text` | `pk_bxa2HCdsT7CKwVSdem8fjS8rW` | `pk_bxa2HCdsT7CKwVSdem8fjS8rW` |
| `3` | `BarCode` | `text` | `text` | `3046920029759` | `3046920029759` |
| `4` | `CSS` | `text` | `text` | `body { font-size: 30px; }` | `body { font-size: 30px; }` |
| `5` | `Color` | `color` | `text` | `#000000` | `#000000` |
| `6` | `ColorRGBA` | `color` | `text` | `#000000ff` | `#000000ff` |
| `7` | `CompanyName` | `text` | `text` | `Google` | `Google` |
| `8` | `CountryISO3166Alpha2` | `text` | `text` | `FR` | `FR` |
| `9` | `CurrencyISO4217` | `text` | `text` | `EUR` | `EUR` |
| `10` | `DateISO8601` | `date` | `text` | `2022-07-14` | `7/14/2022` |
| `11` | `DateTimeFormat` | `text` | `text` | `ccc LLL dd` | `ccc LLL dd` |
| `12` | `DirPath` | `text` | `text` | `/Users/dexter/Desktop` | `/Users/dexter/Desktop` |
| `13` | `DomainName` | `url` | `url` | `myservice.toto.com` | `myservice.toto.com` |
| `14` | `Email` | `email` | `email` | `dexter@caramail.com` | `dexter@caramail.com` |
| `15` | `EmbeddedObject` | `text` | `text` | `{}` | `{}` |
| `16` | `Emoji` | `text` | `text` | `🚀` | `🚀` |
| `17` | `EncryptionKey` | `password` | `text` | `39b65c8b58140bed54c8b9a170f378644f128744a9711ef268ce561a360eb2eee6dbd2fd1ce7a743167e0cff5d7ca13cbdd2bded2b72c58d30caed990c3e04b6` | `39b65c8b58140bed54c8b9a170f378644f128744a9711ef268ce561a360eb2eee6dbd2fd1ce7a743167e0cff5d7ca13cbdd2bded2b72c58d30caed990c3e04b6` |
| `18` | `ErrorMessage` | `text` | `text` | `You are not allowed to access this resource` | `You are not allowed to access this resource` |
| `19` | `ExternalServiceId` | `text` | `text` | `ZNHD34AQW4CV7` | `ZNHD34AQW4CV7` |
| `20` | `File` | `file` | `text` | `{"name":"picture.png","path":"/Users/dexter/Desktop/picture.png","type":"image/png"}` | `{"name":"picture.png","path":"/Users/dexter/Desktop/picture.png","type":"image/png"}` |
| `21` | `FileExtension` | `text` | `text` | `png` | `png` |
| `22` | `FileMimeType` | `text` | `text` | `image/png` | `image/png` |
| `23` | `FileName` | `text` | `text` | `picture.png` | `picture.png` |
| `24` | `FilePath` | `text` | `text` | `/Users/dexter/Desktop/picture.png` | `/Users/dexter/Desktop/picture.png` |
| `25` | `FreeTextLong` | `text` | `text` | `On est jeunes et ambitieux. Parfois vicieux. Faut qu'tu te dises que. Tu peux être le prince de la ville si tu veux (si tu veux). Où tu veux (où tu veux) quand tu veux (quand tu veux).` | `On est jeunes et ambitieux. Parfois vicieux. Faut qu'tu te dises que. Tu peux être le prince de la ville si tu veux (si tu veux). Où tu veux (où tu veux) quand tu veux (quand tu veux).` |
| `26` | `FreeTextShort` | `text` | `text` | `Papillon` | `Papillon` |
| `27` | `Geolocation` | `text` | `text` | `{"lat":0,"lng":0}` | `{"lat":0,"lng":0}` |
| `28` | `GitSSHURL` | `text` | `text` | `git@github.com:nodejs/node.git` | `git@github.com:nodejs/node.git` |
| `29` | `HTML` | `text` | `text` | `This is a paragraph
` | `This is a paragraph
` |
| `30` | `HTTPContentType` | `text` | `text` | `application/json` | `application/json` |
| `31` | `HTTPMethod` | `text` | `text` | `GET` | `GET` |
| `32` | `HTTPStatusNumber` | `number` | `numeric` | `201` | `201` |
| `33` | `HostAddress` | `text` | `text` | `123.45.67.89` | `123.45.67.89` |
| `34` | `HostPort` | `number` | `numeric` | `443` | `443` |
| `35` | `IPv4` | `text` | `text` | `255.255.255.255` | `255.255.255.255` |
| `36` | `IPv6` | `text` | `text` | `2001:0db8:85a3:0000:0000:8a2e:0370:7334` | `2001:0db8:85a3:0000:0000:8a2e:0370:7334` |
| `37` | `JSONString` | `text` | `text` | `{"key": "value"}` | `{"key": "value"}` |
| `38` | `JWT` | `password` | `text` | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c` | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c` |
| `39` | `JavaScript` | `text` | `text` | `(()=>{})();` | `(()=>{})();` |
| `40` | `JobTitle` | `text` | `text` | `Senior Software Engineer` | `Senior Software Engineer` |
| `41` | `Markdown` | `text` | `text` | `**Some important stuff**` | `**Some important stuff**` |
| `42` | `NumIndex` | `number` | `numeric` | `0` | `0` |
| `43` | `Password` | `password` | `text` | `fmUUNWXazWH4` | `fmUUNWXazWH4` |
| `44` | `Percentage` | `number` | `decimal` | `0.26` | `26%` |
| `45` | `PersonFirstname` | `text` | `text` | `Dexter` | `Dexter` |
| `46` | `PersonFullname` | `text` | `text` | `Dexter Morgan` | `Dexter Morgan` |
| `47` | `PersonInitials` | `text` | `text` | `DM` | `DM` |
| `48` | `PersonLastname` | `text` | `text` | `Morgan` | `Morgan` |
| `49` | `QRCode` | `text` | `text` | `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACECAYAAABRRIOnAAAAAklEQVR4AewaftIAAAQvSURBVO3BMQ4bWQxEwecPXYGnZUAw4Gl5CK+DDToaYDCS1vZ21Y+fv2D2r4OZOJiJg5k4mImDmTiYiYOZOJiJg5k4mImDmTiYiYOZOJiJg5l48VBv8k0VwxO9yR0Vw5XeRFUMqjf5porhiYOZOJiJg5l48WYVwzv1Jnf0JndUDFd6k3eqGN6pN3mng5k4mImDmXjxYb3JHRXDHb3JlYpB9SZPVAyqN3miN7mjYvikg5k4mImDmXjxl6kYVG9ypTdRFYOqGK5UDH+Tg5k4mImDmXjxl6sYVG9ypTdRFcOV3kRVDH+yg5k4mImDmXjxYRXDN/Umd1QMqjdRvcknVQy/k4OZOJiJg5l48Wa9yX+pYlC9iaoYVG+iKgbVm6iKQfUmd/Qmv7ODmTiYiYOZ+PHzF/5gvckTFYPqTVTF8H9yMBMHM3EwEy8e6k1UxaB6k3eqGFTF8E4Vg+pNrlQMV3qTd6oYPulgJg5m4mAmXjxUMaje5ErF8ERvoioG1ZtcqRjuqBiu9CaqYlAVg+pNVMVwR2+iKoZ3OpiJg5k4mIkXX9abqIrhSm+iKgbVm3xTb6IqBtWbqIrhSm9ypWK40puoiuGJg5k4mImDmXjxZhWD6k1UxaB6kysVwzv1JlcqBtWbqIpB9SZPVAx3VAyqN3mng5k4mImDmXjxUG9ypWJQvckdvYmqGK5UDKo3URXDHRXDlYpB9SaqN1EVg+pNVMWgepNvOpiJg5k4mIkXb1YxqN5EVQyqN7lSMajeRFUMT/Qmd1QMVyqGK72JqhhUb6IqBtWbqIrhnQ5m4mAmDmbixYdVDE/0JqpiUL3JExWD6k2u9CZ3VAxXepMnehNVMTxxMBMHM3EwEy++rDdRFYPqTVTFoHoTVTFc6U2u9CZXKgbVm3xSxXClYlC9yTsdzMTBTBzMxIsP601UxaB6kyu9iaoYVG/yThWD6k2e6E2e6E1UxaAqhnc6mImDmTiYiR8/f+EP1pvcUTE80ZtcqRju6E2uVAzfdDATBzNxMBMvHupNvqliUBXDHb3JlYrhSsWgepMrvYmqGK5UDKo3uaNieOJgJg5m4mAmXrxZxfBOvcmV3kRVDHdUDFd6E1Ux3FEx3NGbqIrhmw5m4mAmDmbixYf1JndUDL+TikH1Jld6kycqBtWbqIpB9SaqYnjiYCYOZuJgJl785XoTVTGo3uSJikH1JlcqBtWbqN7kSm/ySQczcTATBzPx4i/Tm6iK4UrFoHqTKxWD6k1UxXBHxaB6kysVg+pN3ulgJg5m4mAmXnxYxfBJFcMTvcmVikH1Jld6k3eqGL7pYCYOZuJgJl68WW/yTb3JHRXDlYpB9SaqYrjSm6iKQfUmd/Qm33QwEwczcTATP37+gtm/DmbiYCYOZuJgJg5m4mAmDmbiYCYOZuJgJg5m4mAmDmbiYCYOZuIfrRmHZL9VL3gAAAAASUVORK5CYII=` | `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACECAYAAABRRIOnAAAAAklEQVR4AewaftIAAAQvSURBVO3BMQ4bWQxEwecPXYGnZUAw4Gl5CK+DDToaYDCS1vZ21Y+fv2D2r4OZOJiJg5k4mImDmTiYiYOZOJiJg5k4mImDmTiYiYOZOJiJg5l48VBv8k0VwxO9yR0Vw5XeRFUMqjf5porhiYOZOJiJg5l48WYVwzv1Jnf0JndUDFd6k3eqGN6pN3mng5k4mImDmXjxYb3JHRXDHb3JlYpB9SZPVAyqN3miN7mjYvikg5k4mImDmXjxl6kYVG9ypTdRFYOqGK5UDH+Tg5k4mImDmXjxl6sYVG9ypTdRFcOV3kRVDH+yg5k4mImDmXjxYRXDN/Umd1QMqjdRvcknVQy/k4OZOJiJg5l48Wa9yX+pYlC9iaoYVG+iKgbVm6iKQfUmd/Qmv7ODmTiYiYOZ+PHzF/5gvckTFYPqTVTF8H9yMBMHM3EwEy8e6k1UxaB6k3eqGFTF8E4Vg+pNrlQMV3qTd6oYPulgJg5m4mAmXjxUMaje5ErF8ERvoioG1ZtcqRjuqBiu9CaqYlAVg+pNVMVwR2+iKoZ3OpiJg5k4mIkXX9abqIrhSm+iKgbVm3xTb6IqBtWbqIrhSm9ypWK40puoiuGJg5k4mImDmXjxZhWD6k1UxaB6kysVwzv1JlcqBtWbqIpB9SZPVAx3VAyqN3mng5k4mImDmXjxUG9ypWJQvckdvYmqGK5UDKo3URXDHRXDlYpB9SaqN1EVg+pNVMWgepNvOpiJg5k4mIkXb1YxqN5EVQyqN7lSMajeRFUMT/Qmd1QMVyqGK72JqhhUb6IqBtWbqIrhnQ5m4mAmDmbixYdVDE/0JqpiUL3JExWD6k2u9CZ3VAxXepMnehNVMTxxMBMHM3EwEy++rDdRFYPqTVTFoHoTVTFc6U2u9CZXKgbVm3xSxXClYlC9yTsdzMTBTBzMxIsP601UxaB6kyu9iaoYVG/yThWD6k2e6E2e6E1UxaAqhnc6mImDmTiYiR8/f+EP1pvcUTE80ZtcqRju6E2uVAzfdDATBzNxMBMvHupNvqliUBXDHb3JlYrhSsWgepMrvYmqGK5UDKo3uaNieOJgJg5m4mAmXrxZxfBOvcmV3kRVDHdUDFd6E1Ux3FEx3NGbqIrhmw5m4mAmDmbixYf1JndUDL+TikH1Jld6kycqBtWbqIpB9SaqYnjiYCYOZuJgJl785XoTVTGo3uSJikH1JlcqBtWbqN7kSm/ySQczcTATBzPx4i/Tm6iK4UrFoHqTKxWD6k1UxXBHxaB6kysVg+pN3ulgJg5m4mAmXnxYxfBJFcMTvcmVikH1Jld6k3eqGL7pYCYOZuJgJl68WW/yTb3JHRXDlYpB9SaqYrjSm6iKQfUmd/Qm33QwEwczcTATP37+gtm/DmbiYCYOZuJgJg5m4mAmDmbiYCYOZuJgJg5m4mAmDmbiYCYOZuIfrRmHZL9VL3gAAAAASUVORK5CYII=` |
| `50` | `SQLQuery` | `text` | `text` | `select id, name from users limit 10;` | `select id, name from users limit 10;` |
| `51` | `SSHPrivateKey` | `password` | `text` | `-----BEGIN RSA PRIVATE KEY-----\nfhdsjkdsFDSFDSfgjfkhdsjf\n-----END RSA PRIVATE KEY-----` | `-----BEGIN RSA PRIVATE KEY-----\nfhdsjkdsFDSFDSfgjfkhdsjf\n-----END RSA PRIVATE KEY-----` |
| `52` | `SSHPublicKey` | `text` | `text` | `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG1G1clVyaD6+RGzzPAbyHEiRZQ/+xkSXblmZIOHgY7E` | `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG1G1clVyaD6+RGzzPAbyHEiRZQ/+xkSXblmZIOHgY7E` |
| `53` | `SearchQuery` | `search` | `search` | `Nike Streakfly` | `Nike Streakfly` |
| `54` | `SemVerVersion` | `text` | `text` | `1.2.3` | `1.2.3` |
| `55` | `ShellCommand` | `text` | `text` | `sudo systemctl restart nginx` | `sudo systemctl restart nginx` |
| `56` | `Slug` | `text` | `text` | `title-of-seo-friendly-article` | `title-of-seo-friendly-article` |
| `57` | `Time` | `time` | `numeric` | `10:00` | `10:00` |
| `58` | `Timestamp` | `number` | `numeric` | `1628359209` | `1628359209` |
| `59` | `UIntDuration` | `number` | `numeric` | `3600` | `3,600` |
| `60` | `UIntQuantity` | `number` | `numeric` | `10` | `10` |
| `61` | `URL` | `url` | `url` | `https://myservice.toto.com` | `https://myservice.toto.com` |
| `62` | `URLPath` | `text` | `text` | `/posts/1` | `/posts/1` |
| `63` | `UUID` | `text` | `text` | `dd9670e7-1dd5-4155-85c2-335714799ff7` | `dd9670e7-1dd5-4155-85c2-335714799ff7` |
| `64` | `Username` | `text` | `text` | `dmorgan` | `dmorgan` |
| `65` | `Year` | `number` | `numeric` | `2025` | `2025` |
| `66` | `YesNo` | `text` | `text` | `Y` | `Y` |
file: ./content/docs/references/policies.mdx
meta: {
"title": "Policies",
"description": "libmodulor provides some policies that are commonly used in apps."
}
file: ./content/docs/references/targets.mdx
meta: {
"title": "Targets",
"description": "libmodulor provides targets that are commonly used in apps."
}