Development
Complete guide to developing resources for ESX framework.
This guide covers everything you need to know about developing custom resources for ESX framework.
Development Environment Setup
Prerequisites
- Code Editor: VS Code with Lua extensions recommended
- Git: For version control
- Database Tool: HeidiSQL, phpMyAdmin, or similar
- ESX Server: Running development server
Recommended VS Code Extensions
{
"recommendations": [
"sumneko.lua",
"actboy168.lua-debug",
"keyring.lua",
"koihik.vscode-lua-format",
"trixnz.vscode-lua"
]
}
Development Server Setup
Create a separate development server configuration:
# server-dev.cfg
set sv_hostname "ESX Development Server"
set sv_maxclients 4
sv_licenseKey "your_license_key"
# Developer permissions
add_ace group.admin command allow
add_ace group.admin resource allow
add_principal identifier.steam:your_steam_id group.admin
# Fast restart for development
sv_scriptHookAllowed 1
set es_enableCustomData 1
set esx_multicharacter_enabled true
Resource Structure
Standard ESX Resource Structure
esx_resourcename/
├── client/
│ ├── main.lua
│ ├── events.lua
│ └── utils.lua
├── server/
│ ├── main.lua
│ ├── events.lua
│ └── callbacks.lua
├── shared/
│ ├── config.lua
│ └── locale.lua
├── html/ # For NUI resources
│ ├── index.html
│ ├── style.css
│ └── script.js
├── installation/ # Database files
│ └── esx_resourcename.sql
├── locales/ # Translation files
│ ├── en.lua
│ ├── es.lua
│ └── fr.lua
├── fxmanifest.lua
└── README.md
fxmanifest.lua Template
fx_version 'cerulean'
game 'gta5'
author 'Your Name <[email protected]>'
description 'ESX Resource Description'
version '1.0.0'
repository 'https://github.com/yourusername/esx_resourcename'
shared_scripts {
'@es_extended/imports.lua',
'@es_extended/locale.lua',
'locales/en.lua',
'shared/*.lua'
}
client_scripts {
'client/*.lua'
}
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/*.lua'
}
ui_page 'html/index.html' -- For NUI resources
files {
'html/index.html',
'html/style.css',
'html/script.js'
}
dependencies {
'es_extended',
'oxmysql'
}
Core Integration
Getting ESX Object
-- Modern method (ESX Legacy)
ESX = exports['es_extended']:getSharedObject()
-- Legacy method (still supported)
ESX = nil
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
-- Alternative using imports
-- @es_extended/imports.lua provides global ESX object
Player Data Management
Getting Player Data
-- Server-side
local xPlayer = ESX.GetPlayerFromId(source)
if xPlayer then
local playerData = xPlayer
local identifier = xPlayer.identifier
local job = xPlayer.job
local money = xPlayer.getMoney()
end
-- Client-side
local playerData = ESX.GetPlayerData()
if playerData then
local job = playerData.job
local accounts = playerData.accounts
end
Player Events
-- Client-side: Listen for player data updates
RegisterNetEvent('esx:playerLoaded', function(xPlayer)
ESX.PlayerData = xPlayer
-- Initialize your resource after player loads
end)
RegisterNetEvent('esx:setJob', function(job)
ESX.PlayerData.job = job
-- Handle job updates
end)
RegisterNetEvent('esx:setAccountMoney', function(account)
-- Handle money updates
for i=1, #ESX.PlayerData.accounts do
if ESX.PlayerData.accounts[i].name == account.name then
ESX.PlayerData.accounts[i] = account
break
end
end
end)
-- Server-side: Player management
AddEventHandler('esx:playerLoaded', function(playerId, xPlayer)
-- Handle player loading
end)
AddEventHandler('esx:playerDropped', function(playerId, reason)
-- Handle player disconnect
end)
Database Integration
Using oxmysql/mysql-async
-- SELECT query
MySQL.Async.fetchAll('SELECT * FROM users WHERE job = @job', {
['@job'] = jobName
}, function(result)
if result[1] then
-- Handle results
end
end)
-- SELECT single row
MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', {
['@identifier'] = xPlayer.identifier
}, function(result)
if result then
-- Handle single result
end
end)
-- INSERT query
MySQL.Async.execute('INSERT INTO my_table (identifier, data) VALUES (@identifier, @data)', {
['@identifier'] = xPlayer.identifier,
['@data'] = json.encode(data)
}, function(affectedRows)
if affectedRows > 0 then
-- Success
end
end)
-- UPDATE query
MySQL.Async.execute('UPDATE users SET accounts = @accounts WHERE identifier = @identifier', {
['@accounts'] = json.encode(accounts),
['@identifier'] = xPlayer.identifier
}, function(affectedRows)
-- Handle update
end)
Modern oxmysql (Promise-based)
-- Using promises (recommended)
local result = MySQL.query.await('SELECT * FROM users WHERE job = ?', {jobName})
if result[1] then
-- Handle results
end
-- With error handling
local success, result = pcall(MySQL.query.await, 'SELECT * FROM users WHERE identifier = ?', {xPlayer.identifier})
if success and result[1] then
-- Handle success
else
print('Database query failed')
end
Job System
Creating Custom Jobs
Add jobs to the database:
-- Insert job
INSERT INTO jobs (name, label) VALUES ('mechanic', 'Mechanic');
-- Insert job grades
INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES
('mechanic', 0, 'trainee', 'Trainee', 200, '{}', '{}'),
('mechanic', 1, 'mechanic', 'Mechanic', 400, '{}', '{}'),
('mechanic', 2, 'experienced', 'Experienced Mechanic', 600, '{}', '{}'),
('mechanic', 3, 'chief', 'Chief Mechanic', 800, '{}', '{}');
Job Management Functions
-- Server-side: Job management
local xPlayer = ESX.GetPlayerFromId(source)
-- Set player job
xPlayer.setJob('mechanic', 2)
-- Check job permissions
if xPlayer.job.name == 'police' and xPlayer.job.grade >= 3 then
-- Allow police captain+ actions
end
-- Get all players with specific job
local mechanics = ESX.GetExtendedPlayers('job', 'mechanic')
-- Client-side: Job checking
local playerData = ESX.GetPlayerData()
if playerData.job.name == 'mechanic' then
-- Mechanic-specific functionality
end
Society System Integration
-- Server-side: Society functions
TriggerEvent('esx_society:getOnlinePlayers', function(players)
-- Get online players
end)
-- Get society account
TriggerEvent('esx_addonaccount:getSharedAccount', 'society_mechanic', function(account)
if account then
local balance = account.money
-- Use society money
end
end)
-- Add money to society
TriggerEvent('esx_addonaccount:getSharedAccount', 'society_mechanic', function(account)
if account then
account.addMoney(amount)
end
end)
Money & Account System
Account Management
-- Server-side: Money functions
local xPlayer = ESX.GetPlayerFromId(source)
-- Get money
local cash = xPlayer.getMoney()
local bankMoney = xPlayer.getAccount('bank').money
local blackMoney = xPlayer.getAccount('black_money').money
-- Add money
xPlayer.addMoney(amount)
xPlayer.addAccountMoney('bank', amount)
-- Remove money
if xPlayer.getMoney() >= amount then
xPlayer.removeMoney(amount)
end
-- Set money
xPlayer.setMoney(amount)
xPlayer.setAccountMoney('bank', amount)
-- Client-side: Account checking
local playerData = ESX.GetPlayerData()
for i=1, #playerData.accounts do
if playerData.accounts[i].name == 'bank' then
local bankMoney = playerData.accounts[i].money
break
end
end
Inventory System
Item Management
-- Server-side inventory functions
local xPlayer = ESX.GetPlayerFromId(source)
-- Add item
xPlayer.addInventoryItem('bread', 1)
-- Remove item
xPlayer.removeInventoryItem('water', 1)
-- Get item count
local itemCount = xPlayer.getInventoryItem('phone').count
-- Check if player has item
if xPlayer.getInventoryItem('lockpick').count > 0 then
-- Player has lockpick
end
-- Get weight
local currentWeight = xPlayer.getWeight()
local maxWeight = xPlayer.maxWeight
-- Check if can carry item
if xPlayer.canCarryItem('bread', 5) then
xPlayer.addInventoryItem('bread', 5)
end
Useable Items
-- Server-side: Register useable item
ESX.RegisterUsableItem('bread', function(source)
local xPlayer = ESX.GetPlayerFromId(source)
if xPlayer.getInventoryItem('bread').count > 0 then
xPlayer.removeInventoryItem('bread', 1)
TriggerClientEvent('esx_basicneeds:onEat', source, 'bread')
xPlayer.showNotification('You ate bread')
end
end)
-- Client-side: Handle item usage
RegisterNetEvent('esx_basicneeds:onEat', function(item)
-- Client-side effects for eating
local ped = PlayerPedId()
-- Play eating animation, etc.
end)
Vehicle System
Vehicle Management
-- Server-side: Vehicle functions
local xPlayer = ESX.GetPlayerFromId(source)
-- Get player vehicles
MySQL.Async.fetchAll('SELECT * FROM owned_vehicles WHERE owner = @owner', {
['@owner'] = xPlayer.identifier
}, function(vehicles)
-- Handle vehicles
end)
-- Add vehicle to player
local vehicleData = {
owner = xPlayer.identifier,
plate = 'ABC123',
vehicle = json.encode({model = GetHashKey('adder'), plate = 'ABC123'}),
type = 'car',
job = nil,
stored = 1
}
MySQL.Async.execute('INSERT INTO owned_vehicles (owner, plate, vehicle, type, job, stored) VALUES (@owner, @plate, @vehicle, @type, @job, @stored)', vehicleData)
Vehicle Keys Integration
-- Give keys to player (if using esx_vehiclelock or similar)
TriggerEvent('esx_vehiclelock:giveKeys', source, plate)
-- Remove keys from player
TriggerEvent('esx_vehiclelock:removeKeys', source, plate)
UI Development (NUI)
Basic NUI Setup
<!-- html/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESX Resource UI</title>
<link rel="stylesheet" href="style.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div id="container" style="display: none;">
<div id="header">
<h1>My ESX Resource</h1>
<button id="close-btn">×</button>
</div>
<div id="content">
<!-- Your UI content here -->
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* html/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: transparent;
color: white;
}
#container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 400px;
background: rgba(0, 0, 0, 0.9);
border-radius: 10px;
border: 1px solid #333;
}
#header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #333;
}
#close-btn {
background: #ff4757;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
}
#content {
padding: 20px;
}
// html/script.js
$(document).ready(function() {
// Hide UI on ESC key
document.onkeyup = function(data) {
if (data.which == 27) {
closeUI();
}
}
// Close button click
$('#close-btn').click(function() {
closeUI();
});
// Listen for messages from Lua
window.addEventListener('message', function(event) {
const data = event.data;
switch(data.action) {
case 'open':
openUI(data.data);
break;
case 'close':
closeUI();
break;
case 'updateData':
updateUI(data.data);
break;
}
});
});
function openUI(data) {
$('#container').fadeIn(300);
$.post('https://esx_myresource/uiLoaded', JSON.stringify({}));
}
function closeUI() {
$('#container').fadeOut(300);
$.post('https://esx_myresource/closeUI', JSON.stringify({}));
}
function updateUI(data) {
// Update UI with new data
}
Lua NUI Integration
-- Client-side NUI management
local isUIOpen = false
-- Open UI
function OpenUI(data)
if isUIOpen then return end
isUIOpen = true
SetNuiFocus(true, true)
SendNUIMessage({
action = 'open',
data = data
})
end
-- Close UI
function CloseUI()
if not isUIOpen then return end
isUIOpen = false
SetNuiFocus(false, false)
SendNUIMessage({
action = 'close'
})
end
-- NUI Callbacks
RegisterNUICallback('uiLoaded', function(data, cb)
-- UI has loaded
cb('ok')
end)
RegisterNUICallback('closeUI', function(data, cb)
CloseUI()
cb('ok')
end)
-- Export functions for other resources
exports('OpenUI', OpenUI)
exports('CloseUI', CloseUI)
Event System
Custom Events
-- Server-side events
RegisterNetEvent('esx_myresource:doSomething', function(data)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end
-- Validate data
if not data or not data.value then
return
end
-- Process request
local success = processRequest(xPlayer, data)
-- Send response
TriggerClientEvent('esx_myresource:requestResult', source, success)
end)
-- Client-side events
RegisterNetEvent('esx_myresource:requestResult', function(success)
if success then
ESX.ShowNotification('Request successful!')
else
ESX.ShowNotification('Request failed!', 'error')
end
end)
Callbacks
-- Server-side callback
ESX.RegisterServerCallback('esx_myresource:getData', function(source, cb, playerId)
local xPlayer = ESX.GetPlayerFromId(playerId)
if xPlayer then
cb(xPlayer)
else
cb(false)
end
end)
-- Client-side callback usage
ESX.TriggerServerCallback('esx_myresource:getData', function(playerData)
if playerData then
-- Use player data
end
end, GetPlayerServerId(PlayerId()))
Commands
Creating Commands
-- Server-side command with ESX integration
RegisterCommand('givemoney', function(source, args, rawCommand)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end
-- Check permissions
if xPlayer.getGroup() ~= 'admin' then
xPlayer.showNotification('You need admin permissions', 'error')
return
end
local targetId = tonumber(args[1])
local amount = tonumber(args[2])
if not targetId or not amount then
xPlayer.showNotification('Usage: /givemoney [id] [amount]', 'error')
return
end
local xTarget = ESX.GetPlayerFromId(targetId)
if not xTarget then
xPlayer.showNotification('Player not found', 'error')
return
end
xTarget.addMoney(amount)
xPlayer.showNotification('Money given successfully')
xTarget.showNotification('You received $' .. amount)
end, false)
-- Client-side command
RegisterCommand('showjob', function(source, args, rawCommand)
local playerData = ESX.GetPlayerData()
ESX.ShowNotification('Your job: ' .. playerData.job.label .. ' - Grade: ' .. playerData.job.grade_label)
end, false)
Configuration Management
config.lua Template
Config = {}
Config.Locale = 'en'
-- General settings
Config.Debug = false
-- Feature toggles
Config.EnableFeatureA = true
Config.EnableFeatureB = false
-- Timing settings
Config.Cooldowns = {
action1 = 5000, -- 5 seconds
action2 = 30000, -- 30 seconds
}
-- Job permissions
Config.AuthorizedJobs = {
'police',
'ambulance',
'mechanic'
}
Config.JobPermissions = {
mechanic = {
repair = 0, -- Grade 0+
advanced = 2, -- Grade 2+
boss = 3 -- Grade 3+
},
police = {
arrest = 0,
impound = 1,
commander = 3
}
}
-- Locations
Config.Locations = {
mechanic_shop = {
coords = vector3(123.45, 678.90, 12.34),
heading = 90.0,
radius = 2.0,
blip = {
sprite = 446,
color = 2,
scale = 0.8,
name = "Mechanic Shop"
}
}
}
-- Items and pricing
Config.Items = {
repair_kit = {
item = 'repair_kit',
price = 500,
label = 'Repair Kit'
}
}
-- Money settings
Config.Prices = {
repair = 1000,
paint = 500
}
Localization
Locale Files
-- locales/en.lua
Locales['en'] = {
-- Notifications
['not_enough_money'] = 'You don\'t have enough money',
['action_completed'] = 'Action completed successfully',
['invalid_amount'] = 'Invalid amount',
-- Jobs
['job_mechanic'] = 'Mechanic',
['job_police'] = 'Police Officer',
-- Items
['item_repair_kit'] = 'Repair Kit',
['item_used'] = 'You used %s',
-- Commands
['command_usage'] = 'Usage: %s',
['player_not_found'] = 'Player not found',
['no_permission'] = 'You don\'t have permission',
}
-- locales/es.lua
Locales['es'] = {
-- Notifications
['not_enough_money'] = 'No tienes suficiente dinero',
['action_completed'] = 'Acción completada exitosamente',
['invalid_amount'] = 'Cantidad inválida',
-- Jobs
['job_mechanic'] = 'Mecánico',
['job_police'] = 'Oficial de Policía',
-- Items
['item_repair_kit'] = 'Kit de Reparación',
['item_used'] = 'Usaste %s',
-- Commands
['command_usage'] = 'Uso: %s',
['player_not_found'] = 'Jugador no encontrado',
['no_permission'] = 'No tienes permisos',
}
Using Translations
-- In your scripts
local message = _U('not_enough_money')
xPlayer.showNotification(message, 'error')
-- With parameters
local message = _U('item_used', 'Repair Kit')
ESX.ShowNotification(message)
Testing & Debugging
Debug Functions
-- Debug utility
local function DebugPrint(...)
if Config.Debug then
print('^3[ESX-MYRESOURCE]^7', ...)
end
end
-- Server-side debugging
local function LogAction(xPlayer, action, data)
if Config.Debug then
print(string.format('^3[ESX-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s',
xPlayer.getName(),
xPlayer.identifier,
action,
json.encode(data)
))
end
end
Testing Checklist
- Resource starts without errors
- ESX object initializes properly
- Database connections work
- Player events trigger correctly
- Jobs and permissions work
- Money transactions function
- Items can be used/given/removed
- UI opens/closes properly
- Commands execute with proper permissions
- Localization works
- No console errors or warnings
Best Practices
Code Organization
- Follow ESX Conventions: Use ESX naming conventions and patterns
- Use Callbacks: For data requests between client and server
- Validate Everything: Never trust client input
- Handle Errors: Use pcall for database operations
- Performance: Avoid unnecessary loops and timers
Security
- Server Validation: Always validate on the server
- Permission Checks: Verify job/permissions before actions
- Rate Limiting: Prevent spam/abuse
- Secure Events: Use source validation
Performance
- Efficient Queries: Use proper database indexes
- Caching: Cache frequently accessed data
- Resource Cleanup: Clean up on resource stop
- Minimal UI: Keep NUI lightweight
This comprehensive development guide provides everything needed to create professional ESX resources following best practices and framework conventions.