# Your First ESX Resource (/docs/getting-started/first-resource)

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](/docs/getting-started/installation) first.

## What we build [#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 [#set-up-your-editor]

Any editor works, but Visual Studio Code with these extensions catches most mistakes before you restart the server:

- [Lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) by sumneko: the Lua language server (autocomplete, diagnostics).
- [CfxLua IntelliSense](https://marketplace.visualstudio.com/items?itemName=overextended.cfxlua-vscode) by overextended: FiveM natives and Lua 5.4 syntax used by FiveM.
- [ESX VSCode](https://marketplace.visualstudio.com/items?itemName=Knoblauchbrot.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:

```json title=".vscode/settings.json"
{
  "Lua.diagnostics.disable": ["undefined-global"],
  "Lua.diagnostics.globals": ["ESX", "Config"]
}
```

## Folder structure [#folder-structure]

Create a folder `my_first_script` inside your `resources` folder (or a category folder such as `resources/[local]`).

<Files>
  <Folder name="my_first_script" defaultOpen>
    <File name="fxmanifest.lua" />
    <File name="config.lua" />
    <Folder name="client" defaultOpen>
      <File name="main.lua" />
    </Folder>
    <Folder name="server" defaultOpen>
      <File name="main.lua" />
    </Folder>
  </Folder>
</Files>

## Build it [#build-it]

<Steps>
<Step>
### Write the manifest [#write-the-manifest]

`fxmanifest.lua` tells FiveM which files to load and where.

```lua title="fxmanifest.lua"
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.lua` runs `ESX = exports['es_extended']:getSharedObject()` inside your resource. On the client it also keeps `ESX.PlayerData` up to date and sets `ESX.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.lua` **before** your own files, otherwise `ESX` is nil while they run.
</Step>

<Step>
### Add a config file [#add-a-config-file]

```lua title="config.lua"
Config = {}

Config.WaterPrice = 25        -- price in cash
Config.WaterItem = 'water'    -- item name from the items table
```

The 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.
</Step>

<Step>
### Write the server side [#write-the-server-side]

```lua title="server/main.lua"
-- /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 the `xPlayer` of the caller to your function. Because `allowConsole` is `false`, the console cannot run it, but the `xPlayer` check is still a good habit: from the console `xPlayer` is `false`.
- 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 `source` first, then the function `cb` to 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.
</Step>

<Step>
### Write the client side [#write-the-client-side]

```lua title="client/main.lua"
-- 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:

```lua title="client/main.lua"
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)
```
</Step>

<Step>
### Start and test [#start-and-test]

Add the resource to `server.cfg` **below** the ESX lines:

```bash title="server.cfg"
ensure es_extended
ensure [core]
ensure [esx_addons]
ensure my_first_script
```

Then 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`).
</Step>
</Steps>

## Getting the player on the server [#getting-the-player-on-the-server]

Most server code starts by turning a `source` into an `xPlayer`:

```lua title="server/main.lua"
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](/docs/es_extended/server/xplayer).

## Security basics for your first script [#security-basics-for-your-first-script]

- **Never trust the client.** A cheater can trigger any `RegisterNetEvent` with 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)` and `xPlayer.job.name`.
- **Prefer callbacks over "give me X" events.** A server event named `my_script:giveMoney` that takes an amount is the classic exploit.

## Common mistakes [#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](/docs/guides/add-items). |

## Next steps [#next-steps]

- Learn the conventions that keep scripts fast in [Coding practices](/docs/getting-started/coding-practices).
- Read how callbacks work in detail in [Callbacks](/docs/guides/callbacks).
- Build a full job with grades and salaries in [Create a job](/docs/guides/create-a-job).

## Related [#related]

- [xPlayer reference](/docs/es_extended/server/xplayer)
- [Server functions](/docs/es_extended/server/functions)
- [Client functions](/docs/es_extended/client/functions)
- [Commands](/docs/es_extended/commands)
