About lua-best-practices
A Lua practices guide for FiveM resource developers, using Overextended resources and ox_lib as its reference. It covers loading code and data, manifests, configuration, runtime APIs, and code layout.
Key features
- 01Load code and data with require and lib.load
- 02Organize resource manifests and entry scripts
- 03Manage settings through convars
- 04Use ox_lib cache and runtime APIs
- 05Apply CfxLua syntax and annotations
Use cases
- 01Planning a FiveM resource layout
- 02Implementing Lua modules and data loading
- 03Configuring ox_lib runtime features
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ overextended/ skills --skill lua-best-practicesAfter installing, type/lua-best-practicesin Claude Code to confirm it loaded.
Bundled filesagents/openai.yamlPATTERNS.md
Details
From the project READMELua best practices
Write resource Lua the way Overextended writes ox_inventory, ox_target and ox_fuel: one entry script per side, every other file a module loaded with require, data loaded with lib.load, settings read from convars, and ox_lib's cache and lib.* modules instead of hand-rolled loops. Checked against ox_lib 3.40.3. Read the ox resources line of .ox/config.md (walk up from the current directory) for the server's ox_lib version; if it is older, check anything below against that version's source.
Including @ox_lib/init.lua replaces the global require with ox_lib's own loader. It resolves paths from the resource root, not the calling file, and it can load files from other resources. Everything below assumes it.
Loading code and data
| Call | Use it for | How it behaves |
|---|---|---|
require 'client.utils' | Code modules: files that define functions or state and return a table | Runs the file once per resource and side, then returns the cached value to every later caller, so every module shares one instance. Errors on a circular require. A file that returns nothing gives true. |
lib.load 'data.stations' | Data files (data/*.lua returning a table), and files run for their side effects | Runs the file on every call and returns a fresh value; nothing is cached. Takes an optional environment table as its second argument. |
lib.loadJson 'data.prices' | JSON data files | Reads data/prices.json and returns the decoded table. |
require '@ox_core.lib.init' | Another resource's Lua library | Runs that file inside your resource: you get your own copy, not shared state. Read another resource's live state through its exports. |
Path rules, shared by all three:
- Dots separate directories and there is no extension:
require 'modules.inventory.server'loadsmodules/inventory/server.lua. File names can't contain dots. require 'client'triesclient.lua, thenclient/init.lua.@resource.pathloads from another resource;@resourcealone loads itsinit.lua.- A file loaded on the client must be listed in the manifest's
files. Server-side files load from disk and need no listing.
Choosing:
- Code goes through
require. Putlocal utils = require 'client.utils'at the top of the file that uses it, and end every module withreturnof its table. - Data goes through
lib.load, called once where the data is consumed (for id, data in pairs(lib.load('data.crafting') or {}) do). It isn't cached, so never call it inside a loop or an event handler. - Read-only config shared by several modules can be a module:
config.luareturns a table and every file doeslocal config = require 'config'. - A file is a manifest script or a module, never both. Listing a file in
client_scriptsand also requiring it runs it twice. - Load optional parts lazily:
if config.ox_target then return require 'client.target' end, or a framework bridge required only in the branch that detects that framework. - Don't hand-roll
LoadResourceFileplusload. ox_inventory's owndata()loader is deprecated in favour oflib.load.
Manifest
fx_version 'cerulean',lua54 'yes'anduse_experimental_fxv2_oal 'yes', as every ox resource has.shared_script '@ox_lib/init.lua'comes before any of the resource's own scripts, andox_libis independencies.- One entry script per side (
client/init.lua,server/init.lua, or a singleinit.luathat ends withrequire 'server'orrequire 'client'depending onlib.context). The entry script requires everything else. fileslists every module, data file and locale the client loads ('client/*.lua','data/*.lua','locales/*.json'). Every client downloads everything infiles, so don't list server-only code there.ox_libs { 'locale', 'table' }loads thoselibmodules when the resource starts. Listinglocalethere also loads the locale strings, which ox_lib prefers over callinglib.locale()yourself. Any otherlib.*module loads on first use, so neverrequireone.
Config
- Settings a server owner tunes are convars named
<resource>:<setting>, read once at the top of the file that uses them:GetConvarInt('ox_target:debug', 0),GetConvarBool('inventory:target', false),GetConvar('ox_target:defaultHotkey', 'LMENU'). Structured settings go throughjson.decode(GetConvar(...)). Owners set them inserver.cfg, so an update never overwrites their changes. - Structured data (locations, items, shops, recipes) lives in
data/*.luafiles returning tables, loaded withlib.load. - A small resource can use a
config.luathat returns a table, required as a module. Follow whichever approach the resource already uses.
Scope
- Every variable, function and module table is
local. A module islocal Utils = {}, its functionsfunction Utils.raycast(...), andreturn Utilsat the end. - A global is a deliberate choice for state every file on that side reads, never a forgotten
local. All of a resource's scripts and modules on one side share one global scope. - In a hot loop, cache a native as a local upvalue:
local GetVehiclePedIsIn = GetVehiclePedIsIn.
Layout
Code reads top to bottom in short steps, with blank lines between them and few long lines.
- One statement per line.
count = 0 return falseis two lines. - A blank line between steps: the guards, the lookups, the work, the reply.
- A guard stays on one line only when it's short and its body is a bare
returnorreturn false:if not player or not player.shop then return false end. A guard that returns a table or does more goes across lines. - A condition too long for one line becomes several guards, never one
ifwrapped across lines. - A call or table constructor that runs past about 120 characters goes one argument or field per line. Shorter ones stay on one line.
- Indent with whatever
.editorconfigor the file uses.
ox_lib at runtime
cacheholds the player's state on the client, refreshed every 100 ms:cache.ped,cache.vehicle(falseon foot),cache.seat,cache.weapon, pluscache.playerIdandcache.serverId. Both sides havecache.resourceandcache.game. Readcache.ped, notPlayerPedId().lib.onCache('vehicle', function(value, oldValue) end)runs when a cached value changes. Use it instead of a loop that polls for the change.- Threads:
CreateThreadandWait. For repeating work use ox_lib'sSetInterval(fn, ms)andClearInterval(id). For "do something near a location" uselib.points.new({ coords = ..., distance = ..., onEnter = ..., onExit = ..., nearby = ... }), not a distance loop. To wait for a value, uselib.waitFor(fn, errMessage, timeout), which errors aftertimeoutms (default 1000). - Callbacks: the server registers
lib.callback.register(name, function(source, ...) end)and the client callslib.callback.await(name, delay, ...).delayis a client-side rate limit in ms, orfalse; a throttled call returns nothing. The client chooses every argument, so the server callback checks each one (type, range, ownership, distance) before acting. - Locales:
locales/en.jsonis the fallback, merged under the language set by theox:localeconvar.locale('key', ...)formats withstring.formatand returns the key itself when it's missing. - Logging:
lib.print.error,warn,info,verbose,debug. Owners filter them with theox:printlevelconvar, orox:printlevel:<resource>for one resource. - Dependencies: check a minimum version at the top of the entry script with
if not lib.checkDependency('ox_lib', '3.30.0', true) then return end. Guard an optional resource withGetResourceState('ox_target') == 'started'.
CfxLua
FiveM's Lua 5.4 runtime adds syntax that ox resources use everywhere. Match it:
- Backtick hashes for constant names:
`WEAPON_UNARMED`is hashed at compile time. Keepjoaat(name)for names built at runtime. - Safe navigation:
data.server?.exportinstead ofdata.server and data.server.export. - Compound assignment:
count += 1. table.create(arraySize, hashSize)to preallocate,table.wipe(t)to clear in place.
Annotations
Annotate module functions with LuaLS comments (---@param, ---@return, ---@class) and narrow with --[[@as OxClient]] where a value's type can't be inferred. ox_lib ships its own annotations, so lib.* calls type-check in the editor.
Examples
Read PATTERNS.md for a full resource layout and an example of each section.