@laot/nuix
v1.0.5
Published
Modular, type-safe TypeScript library for FiveM NUI projects
Readme
NUIX
A type-safe TypeScript helper library for FiveM NUI development. Wraps the most common NUI patterns — fetching data from Lua, listening for messages, formatting strings, and handling translations — in a clean, fully typed API. Zero runtime dependencies, works with any frontend framework.
Table of Contents
Install
# pick your package manager
npm install @laot/nuix
pnpm add @laot/nuix
yarn add @laot/nuix
bun add @laot/nuixQuick Start
1. Event Maps
Before using anything, you'll want to define your events. NUIX uses these maps to infer the exact types for both data you send and responses you get back. You'll typically have two separate maps:
- Callback events — for
fetchNuicalls. Your TS code sends data to Lua, Lua processes it and sends a response back. - Message events — for
onNuiMessagelisteners. Lua pushes data to TS viaSendNUIMessage, no response needed.
import type { NuiEventMap } from "@laot/nuix";
// Things you ASK Lua for (request → response)
interface CallbackEvents extends NuiEventMap {
getPlayer: { data: { id: number }; response: { name: string; level: number } };
sendNotify: { data: { message: string }; response: void };
}
// Things Lua TELLS you about (one-way push)
interface MessageEvents extends NuiEventMap {
showMenu: { data: { items: string[] }; response: void };
hideMenu: { data: void; response: void };
}Keeping them separate isn't mandatory, but it makes your code way easier to reason about — you'll always know which events go where.
2. fetchNui — Typed Lua Callbacks
createFetchNui gives you a typed function that POSTs JSON to https://<resourceName>/<event>, matching RegisterNUICallback on the Lua side. The resource name is automatically grabbed from FiveM's GetParentResourceName().
import { createFetchNui } from "@laot/nuix";
const fetchNui = createFetchNui<CallbackEvents>();
// fully typed — player is { name: string; level: number }
const player = await fetchNui("getPlayer", { id: 1 });
console.log(player.name, player.level);
// void response — you're just notifying Lua, no return value
await fetchNui("sendNotify", { message: "Hello!" });You can also set a timeout to avoid hanging forever if the Lua callback never responds:
const data = await fetchNui("getPlayer", { id: 2 }, { timeout: 5000 });
// rejects with "[NUIX] fetchNui("getPlayer") timed out after 5000ms" if no response3. onNuiMessage — Listening to Lua
Listens for messages from Lua's SendNUIMessage. There are two ways to use it:
Switch-case — one listener that handles every action:
import { onNuiMessage } from "@laot/nuix";
const unsub = onNuiMessage<MessageEvents>((action, data) => {
switch (action) {
case "showMenu":
console.log(data.items);
break;
case "hideMenu":
closeMenu();
break;
}
});
// when you're done listening
unsub();Per-action — filters by action name, and data is fully typed automatically:
const unsub = onNuiMessage<MessageEvents, "showMenu">("showMenu", (data) => {
console.log(data.items); // ✅ typed as string[]
});Both overloads return an UnsubscribeFn — just call it to remove the event listener.
4. luaFormat — String Formatting
A small utility that formats strings using Lua-style placeholders. Handles null and undefined safely instead of crashing.
| Specifier | What it does |
|-----------|--------------------------------------|
| %s | String (null/undefined → "") |
| %d / %i | Integer (floors the value, NaN → 0) |
| %f | Float (NaN → 0) |
| %% | Literal % sign |
import { luaFormat } from "@laot/nuix";
luaFormat("Hello %s, you are level %d", "Laot", 42);
// → "Hello Laot, you are level 42"
luaFormat("Accuracy: %f%%", 99.5);
// → "Accuracy: 99.5%"
luaFormat("Safe: %s %d", undefined, NaN);
// → "Safe: 0"5. Translator (Global)
A global translation system built on top of luaFormat. The idea is simple: Lua sends locale data once (usually on resource start), you register it, and then use _U() anywhere in your UI to get translated strings.
Registration:
import { registerLocales, _U, onNuiMessage } from "@laot/nuix";
import type { NuiEventMap, LocaleRecord } from "@laot/nuix";
interface Events extends NuiEventMap {
setLocales: { data: LocaleRecord; response: void };
showMenu: { data: { items: string[] }; response: void };
}
onNuiMessage<Events>((action, data) => {
switch (action) {
case "setLocales":
registerLocales(data); // store the locale map globally
break;
case "showMenu":
openMenu(data.items);
break;
}
});Usage — anywhere in your app:
// assuming Lua sent: { ui: { greeting: "Hello %s!", level: "Level %d" } }
_U("ui.greeting", "Hi", "Laot"); // → "Hello Laot!"
_U("ui.level", "Lv.", 42); // → "Level 42"
_U("missing.key", "Fallback"); // → "Fallback" (key not found, returns fallback)Adding more translations later without overwriting existing ones:
import { extendLocales } from "@laot/nuix";
extendLocales({ ui: { subtitle: "Overview" } });
// merges into the existing locale map — won't touch other keys
_Uuses dot notation."ui.greeting"looks uplocales.ui.greetingunder the hood.
6. Translator (Isolated)
If you need a translator that's completely independent from the global _U — maybe a component with its own locale scope — use createTranslator:
import { createTranslator } from "@laot/nuix";
const _T = createTranslator({
locales: {
greeting: "Hello %s!",
level: "Level %d",
},
});
_T("greeting", "MISSING", "Laot"); // → "Hello Laot!"
_T("level", "MISSING", 42); // → "Level 42"
_T("no.key", "Not found"); // → "Not found"There's also mergeLocales if you need to deep-merge locale records manually:
import { mergeLocales } from "@laot/nuix";
const base = { ui: { greeting: "Hello %s!" } };
const patch = { ui: { greeting: "Hey %s, welcome back!" } };
const merged = mergeLocales(base, patch);
// merged.ui.greeting → "Hey %s, welcome back!"7. Debug Mode
Pass debug: true to createFetchNui and every call will be logged to the console with the [NUIX] prefix. Super useful during development:
const fetchNui = createFetchNui<CallbackEvents>({ debug: true });
await fetchNui("getPlayer", { id: 1 });
// Console:
// [NUIX] → getPlayer { id: 1 }
// [NUIX] ← getPlayer { name: "Laot", level: 42 }8. Mock Data (Local Development)
When you're building your UI outside of FiveM (like in a regular browser with npm run dev), there's no Lua backend to respond to your fetchNui calls. That's where mockData comes in — it returns pre-defined responses without making any HTTP requests.
const fetchNui = createFetchNui<CallbackEvents>({
debug: true,
mockData: {
// static response — just return this object every time
getPlayer: { name: "DevPlayer", level: 99 },
// dynamic response — receive the data, return something based on it
sendNotify: (data) => {
console.log("Mock notification:", data.message);
},
},
});
const player = await fetchNui("getPlayer", { id: 1 });
// Console:
// [NUIX] → getPlayer { id: 1 }
// [NUIX] ← getPlayer (mock) { name: "DevPlayer", level: 99 }Works great combined with
debug: true— you can see exactly what's being sent and received in the console.
9. isEnvBrowser — Environment Detection
Returns true when running outside FiveM (regular browser). Uses the absence of FiveM's invokeNative bridge to detect the environment.
import { isEnvBrowser } from "@laot/nuix";
if (isEnvBrowser()) {
// Running in dev browser — skip game-only logic
console.log("Dev mode");
}Note:
fetchNuialready uses this internally — if you're in a browser and nomockDatais configured for the event, it throws a clear error instead of making a doomed HTTP request.
10. disableMockInGame — Mock Control in FiveM
By default, mockData is used everywhere — both in the browser and inside FiveM. If you want mocks to only work during local development but use real Lua callbacks inside the game, set disableMockInGame: true:
const fetchNui = createFetchNui<CallbackEvents>({
disableMockInGame: true,
mockData: {
getPlayer: { name: "DevPlayer", level: 99 },
},
});
// In browser → returns mock { name: "DevPlayer", level: 99 }
// In FiveM → calls real RegisterNUICallback("getPlayer", ...)| Environment | disableMockInGame: false (default) | disableMockInGame: true |
|---|---|---|
| Browser + mock exists | Mock response ✅ | Mock response ✅ |
| Browser + no mock | Error thrown ❌ | Error thrown ❌ |
| FiveM + mock exists | Mock response | Real fetch |
| FiveM + no mock | Real fetch | Real fetch |
Lua Side
Here's how the Lua side connects to everything above:
-- Responds to fetchNui("getPlayer", { id = ... })
RegisterNUICallback("getPlayer", function(data, cb)
local player = GetPlayerData(data.id)
cb({ name = player.name, level = player.level })
end)
-- Pushes a message to onNuiMessage listeners
SendNUIMessage({ action = "showMenu", data = { items = {"Pistol", "Rifle"} } })
-- Sends locale data for registerLocales
SendNUIMessage({ action = "setLocales", data = Locales })API Reference
Functions
| Export | Description |
|---|---|
| createFetchNui<TMap>(options?) | Returns a typed fetchNui function. Supports debug, mockData, and disableMockInGame options. |
| isEnvBrowser() | Returns true when running outside FiveM (regular browser). |
| onNuiMessage<TMap>(handler) | Listens to all NUI messages — use with a switch-case. |
| onNuiMessage<TMap, K>(action, handler) | Listens to a single action — data is automatically typed. |
| luaFormat(template, ...args) | Lua-style string formatter with %s / %d / %f support. |
| registerLocales(locales) | Sets the global locale map (replaces the current one). |
| extendLocales(...records) | Merges new entries into the existing global locale map. |
| _U(key, fallback, ...args) | Global translator — reads from the registered locale map. |
| createTranslator(options) | Returns an isolated translator function with its own locale scope. |
| mergeLocales(...records) | Deep-merges multiple locale records into one. |
Types
| Export | Description |
|---|---|
| NuiEventMap | Base interface for defining event maps. |
| NuiMessagePayload<TData> | Shape of SendNUIMessage payloads ({ action, data }). |
| FetchNuiOptions | Per-call options for fetchNui (e.g. timeout). |
| FetchNuiFactoryOptions<TMap> | Config for createFetchNui (debug, mockData, disableMockInGame). |
| LocaleRecord | Flat or nested string map used for translations. |
| TranslatorOptions | Config for createTranslator. |
| TranslatorFn | Translator function signature ((key, fallback, ...args) => string). |
| FormatArg | Accepted argument types for luaFormat (string \| number \| boolean \| null \| undefined). |
| UnsubscribeFn | Cleanup function returned by onNuiMessage. |
| NuiMessageHandler<TData> | Callback type for NUI message listeners. |
Build
npm run build # outputs ESM + CJS + .d.ts to dist/
npm run typecheck # tsc --noEmitRequires Node.js ≥ 18.
License
MIT © LAOT
