Monospace

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(): void

expose(exposes)

Registers one or more JavaScript functions that become callable from the Lua client.

function expose(exposes: Record<string, (...args: any[]) => unknown>): void
ParameterTypeDescription
exposesRecord<string, Function>An object mapping function names to implementations

Quick Start

src/app.tsx
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)
  },
})
client/init.lua
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 } │               │
└─────────────┘                    └──────────────┘
  1. JavaScript calls expose({ key: fn }) — Functions are stored in a registry
  2. JavaScript sends the registered keys to Lua via fetchNui('exposes', keys)
  3. Lua creates wrapper functions for each key
  4. When Lua calls a wrapper, it sends a SendNUIMessage with type invokeExpose
  5. JavaScript receives the message, looks up the function, calls it, and sends the result back via fetchNui('exposeResult', ...)
  6. 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:

client/expose.lua
---@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 registries

Validation

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.

On this page