expose
Expose JavaScript functions callable from FiveM Lua — bidirectional RPC.
The expose system provides a bidirectional RPC (Remote Procedure Call) mechanism between your NUI JavaScript code and FiveM Lua client. It allows you to register JavaScript functions that can be called directly from Lua, with results returned asynchronously.
Import
import { expose, initExposes } from '@repo/ui/utils/expose'API
initExposes()
Initializes the expose system by registering the internal invokeExpose message listener. Must be called once during app startup.
function initExposes(): voidexpose(exposes)
Registers one or more JavaScript functions that become callable from the Lua client.
function expose(exposes: Record<string, (...args: any[]) => unknown>): void| Parameter | Type | Description |
|---|---|---|
exposes | Record<string, Function> | An object mapping function names to implementations |
Quick Start
import { expose, initExposes } from '@repo/ui/utils/expose'
// 1. Initialize the expose system (once, at app startup)
initExposes()
// 2. Register functions
expose({
getFormData: () => {
return {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
}
},
setTheme: (theme: string) => {
document.body.className = theme
return true
},
calculateTotal: (items: number[]) => {
return items.reduce((sum, item) => sum + item, 0)
},
})local exposes = require 'expose' -- The Lua companion module
-- Call the JS function and get the result
exposes.getFormData({}, function(result, err)
if err then
print('Error:', err)
return
end
print('Name:', result.name)
print('Email:', result.email)
end)
-- Pass arguments
exposes.calculateTotal({ { 10, 20, 30 } }, function(result, err)
print('Total:', result) -- 60
end)How It Works
The expose system uses NUI callbacks as a transport layer:
┌─────────────┐ ┌──────────────┐
│ Lua │ SendNUIMessage │ JavaScript │
│ Client │ ───────────────► │ (NUI) │
│ │ invokeExpose │ │
│ │ { requestId, │ 1. Look up │
│ │ key, args } │ registry │
│ │ │ 2. Call fn │
│ │ NUI Callback │ 3. Return │
│ │ ◄─────────────── │ result │
│ │ exposeResult │ │
│ │ { requestId, │ │
│ │ result/error } │ │
└─────────────┘ └──────────────┘- JavaScript calls
expose({ key: fn })— Functions are stored in a registry - JavaScript sends the registered keys to Lua via
fetchNui('exposes', keys) - Lua creates wrapper functions for each key
- When Lua calls a wrapper, it sends a
SendNUIMessagewith typeinvokeExpose - JavaScript receives the message, looks up the function, calls it, and sends the result back via
fetchNui('exposeResult', ...) - Lua receives the result and calls the original callback
Lua Companion Module
The Lua companion module handles the Lua side of the RPC. Place this in your FiveM resource:
---@class ExposeRegistries
---@type table<string, fun(args: table, callback?: fun(result: any, err: string|nil): nil): nil>
local registries = {}
---@type table<string, fun(result: any, err: string|nil): nil>
local pendingCallbacks = {}
local requestCounter = 0
local function generateRequestId()
requestCounter = requestCounter + 1
return tostring(requestCounter)
end
-- JS → Lua: register exposed keys
RegisterNUICallback('exposes', function(keys, cb)
for _, key in ipairs(keys) do
registries[key] = function(args, callback)
local requestId = generateRequestId()
pendingCallbacks[requestId] = callback
SendNUIMessage({
type = 'invokeExpose',
data = {
requestId = requestId,
key = key,
args = args
}
})
end
end
cb('ok')
end)
-- JS → Lua: return result back to caller
RegisterNUICallback('exposeResult', function(data, cb)
local callback = pendingCallbacks[data.requestId]
if callback then
pendingCallbacks[data.requestId] = nil
callback(data.result, data.error)
end
cb('ok')
end)
return registriesValidation
The expose function validates the input using Zod before registering:
- Keys must be strings
- Values must be functions
If validation fails, an error is logged and the functions are not registered.
Error Handling
If a registered function throws an error or rejects, the error message is sent back to Lua as the second argument of the callback:
exposes.riskyOperation({}, function(result, err)
if err then
print('Operation failed:', err)
return
end
print('Success:', result)
end)The expose system requires both the JavaScript and Lua sides to be set up. Make sure to include the Lua companion module in your FiveM resource's fxmanifest.lua.