Your First ESX Resource
Build your first ESX Legacy resource step by step - fxmanifest with imports.lua, a server command with xPlayer, client notifications and a server callback.
This tutorial builds a small but complete ESX resource from scratch: a /balance command that shows a player's money, and a /buywater command that charges the player on the server and gives them an item. Along the way you learn the pieces almost every ESX script uses: the manifest, the ESX object, xPlayer, commands, notifications and server callbacks.
You need a running ESX Legacy server. If you do not have one yet, follow Installation first.
What we build
| Feature | Side | ESX API used |
|---|---|---|
/balance shows cash and bank | server | ESX.RegisterCommand, xPlayer.getMoney, xPlayer.getAccount, xPlayer.showNotification |
/buywater buys a water bottle | client + server | ESX.TriggerServerCallback, ESX.RegisterServerCallback, xPlayer.removeAccountMoney, xPlayer.addInventoryItem |
| Welcome message on spawn | client | esx:playerLoaded, ESX.ShowNotification |
Set up your editor
Any editor works, but Visual Studio Code with these extensions catches most mistakes before you restart the server:
- Lua by sumneko: the Lua language server (autocomplete, diagnostics).
- CfxLua IntelliSense by overextended: FiveM natives and Lua 5.4 syntax used by FiveM.
- ESX VSCode: snippets and completion for ESX functions.
If the Lua extension reports globals like ESX or CreateThread as undefined, add this to your VS Code settings:
{
"Lua.diagnostics.disable": ["undefined-global"],
"Lua.diagnostics.globals": ["ESX", "Config"]
}Folder structure
Create a folder my_first_script inside your resources folder (or a category folder such as resources/[local]).
Build it
Write the manifest
fxmanifest.lua tells FiveM which files to load and where.
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
name 'my_first_script'
author 'Your Name'
version '1.0.0'
shared_scripts {
'@es_extended/imports.lua', -- defines the global ESX on client and server
'config.lua',
}
client_scripts {
'client/main.lua',
}
server_scripts {
'server/main.lua',
}
dependency 'es_extended'@es_extended/imports.luarunsESX = exports['es_extended']:getSharedObject()inside your resource. On the client it also keepsESX.PlayerDataup to date and setsESX.PlayerLoaded.lua54 'yes'enables Lua 5.4. All ESX core resources use it.dependency 'es_extended'makes FiveM refuse to start your script without ESX, instead of failing later with nil errors.- Load
imports.luabefore your own files, otherwiseESXis nil while they run.
Add a config file
Config = {}
Config.WaterPrice = 25 -- price in cash
Config.WaterItem = 'water' -- item name from the items tableThe config is shared, so the client sees it too. That is fine for prices, but never put webhooks, API keys or other secrets in a shared file. Put them in a server-only file instead.
Write the server side
-- /balance: show cash and bank to the player who typed it
ESX.RegisterCommand('balance', 'user', function(xPlayer, args, showError)
if not xPlayer then
return print('This command can only be used in game.')
end
local cash = xPlayer.getMoney()
local bank = xPlayer.getAccount('bank')
xPlayer.showNotification(('Cash: $%s | Bank: $%s'):format(
ESX.Math.GroupDigits(cash),
ESX.Math.GroupDigits(bank and bank.money or 0)
), 'info')
end, false, { help = 'Show your cash and bank balance' })
-- Callback used by /buywater. The client asks, the server decides.
ESX.RegisterServerCallback('my_first_script:buyWater', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then
return cb(false, 'Player not loaded')
end
if not xPlayer.canCarryItem(Config.WaterItem, 1) then
return cb(false, 'You cannot carry more')
end
-- removeAccountMoney returns false if the player cannot afford it
if not xPlayer.removeAccountMoney('money', Config.WaterPrice, 'Bought water') then
return cb(false, 'Not enough cash')
end
xPlayer.addInventoryItem(Config.WaterItem, 1)
cb(true)
end)Things to notice:
ESX.RegisterCommand(name, group, cb, allowConsole, suggestion)passes thexPlayerof the caller to your function. BecauseallowConsoleisfalse, the console cannot run it, but thexPlayercheck is still a good habit: from the consolexPlayerisfalse.- The second argument is the group that may use the command. ESX commands are always restricted, so with
'admin'normal players would get "access denied". Use'user'for everyone or a table like{ 'admin', 'owner' }. - The callback receives
sourcefirst, then the functioncbto answer with, then any extra arguments the client sent. - All money logic happens on the server. The client only asks; it never says how much to pay.
Write the client side
-- Runs once when the player's character has loaded
RegisterNetEvent('esx:playerLoaded', function(xPlayer)
ESX.ShowNotification(('Welcome back, %s!'):format(xPlayer.firstName or GetPlayerName(PlayerId())), 'success')
end)
-- /buywater: ask the server to buy a water bottle
RegisterCommand('buywater', function()
if not ESX.IsPlayerLoaded() then return end
ESX.TriggerServerCallback('my_first_script:buyWater', function(success, reason)
if success then
ESX.ShowNotification(('You bought water for $%d'):format(Config.WaterPrice), 'success')
else
ESX.ShowNotification(reason or 'Purchase failed', 'error')
end
end)
end, false)
TriggerEvent('chat:addSuggestion', '/buywater', ('Buy a bottle of water for $%d'):format(Config.WaterPrice))ESX.ShowNotification(message, type, length, title, position) uses esx_notify. Valid types are info, success and error.
If you prefer code without nested functions, ESX also has an awaitable version that must run inside a thread or command handler:
RegisterCommand('buywater2', function()
local success, reason = ESX.AwaitServerCallback('my_first_script:buyWater')
ESX.ShowNotification(success and 'Bought water' or reason, success and 'success' or 'error')
end, false)Start and test
Add the resource to server.cfg below the ESX lines:
ensure es_extended
ensure [core]
ensure [esx_addons]
ensure my_first_scriptThen start it without restarting the server: type ensure my_first_script in the server console or txAdmin live console. In game, type /balance, then /buywater and open your inventory (F2 by default).
After every change, run ensure my_first_script again (or restart my_first_script).
Getting the player on the server
Most server code starts by turning a source into an xPlayer:
RegisterNetEvent('my_first_script:doSomething', function()
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end -- not loaded yet or already dropped
print(xPlayer.getIdentifier(), xPlayer.getName(), xPlayer.job.name, xPlayer.getGroup())
end)ESX.GetPlayerFromId returns nil until the player has picked a character, so always check the result. The full list of methods is in the xPlayer reference.
Security basics for your first script
- Never trust the client. A cheater can trigger any
RegisterNetEventwith any arguments. Prices, amounts, item names and target IDs must be decided or validated on the server. - Check distance and job on the server before rewarding something, for example with
xPlayer.getCoords(true)andxPlayer.job.name. - Prefer callbacks over "give me X" events. A server event named
my_script:giveMoneythat takes an amount is the classic exploit.
Common mistakes
| Symptom | Cause |
|---|---|
attempt to index a nil value (global 'ESX') | @es_extended/imports.lua missing in the manifest, or listed after your files, or your resource starts before es_extended. |
attempt to call a nil value (field 'RegisterCommand') on the client | ESX.RegisterCommand is server-only. Use the native RegisterCommand on the client. |
| Command says "access denied" | Wrong group in ESX.RegisterCommand, or the resource.es_extended ACE lines are missing from server.cfg. |
| Callback never answers | The name differs between client and server, or the server function returned without calling cb. |
Item "water" was used but does not exist | The item is not in the items table. See Add items. |
Next steps
- Learn the conventions that keep scripts fast in Coding practices.
- Read how callbacks work in detail in Callbacks.
- Build a full job with grades and salaries in Create a job.
Related
Frequently asked questions
Independent documentation
Last updated on
server.cfg for ESX Legacy
Annotated server.cfg for ESX Legacy - ensure order, mysql_connection_string formats, OneSync, admin ACE permissions and every convar es_extended reads.
Coding Practices for ESX
Best practices for ESX Legacy scripts - naming, locals vs globals, efficient loops, event-driven code, caching, modern natives, Lua 5.4 features and server security.