This codifies the patterns you need for production Roblox development: server authority principles, proper error handling with pcall and result types, memory cleanup with maids, and security validation for remote events. It covers naming conventions (PascalCase for services, camelCase for functions, SCREAMING_SNAKE_CASE for constants), file organization with strict mode, and the service/controller patterns for structuring your game. The anti-patterns section alone is worth it, calling out wait() vs task.wait(), polling loops, and string concatenation in hot paths. Use this when you're setting up a new game architecture or reviewing code that needs to scale beyond the prototype phase.
npx -y skills add dig1t/skills --skill luau-best-practices --agent claude-codeInstalls into .claude/skills of the current project.
Production-quality patterns for Roblox game development.
-- PascalCase: Types, Classes, Services, Modules
type PlayerData = { ... }
local ShopService = {}
local PlayerController = require(...)
-- camelCase: Variables, functions, methods
local playerCount = 0
local function getPlayerData() end
function ShopService:purchaseItem() end
-- SCREAMING_SNAKE_CASE: Constants
local MAX_PLAYERS = 50
local DEFAULT_HEALTH = 100
-- Private with underscore prefix
local function _validateInput() end
local _cache = {}
--!strict
-- 1. Services/imports at top
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Signal = require(ReplicatedStorage.Packages.Signal)
local Types = require(script.Parent.Types)
-- 2. Constants
local MAX_RETRIES = 3
local TIMEOUT = 5
-- 3. Types
type Config = {
enabled: boolean,
maxItems: number,
}
-- 4. Module table
local MyModule = {}
-- 5. Private state
local _initialized = false
local _cache: { [string]: any } = {}
-- 6. Private functions
local function _helperFunction()
end
-- 7. Public API
function MyModule.init()
end
function MyModule.doSomething()
end
-- 8. Return
return MyModule
--!strict
local MyService = {}
local _started = false
function MyService:Start()
assert(not _started, "MyService already started")
_started = true
-- Initialize connections, load data
end
function MyService:Stop()
-- Cleanup for hot-reloading
end
return MyService
--!strict
local MyController = {}
local _player = game:GetService("Players").LocalPlayer
function MyController:Init()
-- Setup without yielding
end
function MyController:Start()
-- Connect events, start loops
end
return MyController
local _data: PlayerData? = nil
local function getData(): PlayerData
if not _data then
_data = loadExpensiveData()
end
return _data
end
-- DataStore, HTTP, any Roblox API that can fail
local success, result = pcall(function()
return dataStore:GetAsync(key)
end)
if not success then
warn("DataStore failed:", result)
return nil
end
return result
type Result<T> =
{ ok: true, value: T } |
{ ok: false, error: string }
local function fetchData(id: string): Result<Data>
local success, data = pcall(function()
return dataStore:GetAsync(id)
end)
if not success then
return { ok = false, error = tostring(data) }
end
return { ok = true, value = data }
end
-- Use assert for things that should never happen
function processPlayer(player: Player)
assert(player, "player is required")
assert(player:IsA("Player"), "expected Player instance")
-- ...
end
See references/error-handling.md for comprehensive patterns.
local connection: RBXScriptConnection
connection = event:Connect(function()
-- handler
end)
-- Later, cleanup:
connection:Disconnect()
local Maid = require(Packages.Maid)
local maid = Maid.new()
maid:GiveTask(event:Connect(handler))
maid:GiveTask(instance)
maid:GiveTask(function()
-- Custom cleanup
end)
-- Cleanup everything at once
maid:Destroy()
local cache = setmetatable({}, { __mode = "v" })
-- Values are garbage collected when no other references exist
cache[key] = expensiveObject
See references/memory.md for leak prevention patterns.
-- BAD: Client tells server what happened
RemoteEvent.OnServerEvent:Connect(function(player, damage)
target.Health -= damage -- Client controls damage!
end)
-- GOOD: Server calculates everything
RemoteEvent.OnServerEvent:Connect(function(player, targetId)
local target = getValidTarget(player, targetId)
if not target then return end
local damage = calculateDamage(player) -- Server calculates
target.Health -= damage
end)
RemoteFunction.OnServerInvoke = function(player, itemId, quantity)
-- Type validation
if typeof(itemId) ~= "string" then return end
if typeof(quantity) ~= "number" then return end
-- Range validation
if quantity < 1 or quantity > 99 then return end
if quantity ~= math.floor(quantity) then return end
-- Business logic validation
if not Items[itemId] then return end
if not canAfford(player, itemId, quantity) then return end
-- Now safe to process
return purchaseItem(player, itemId, quantity)
end
local lastAction: { [Player]: number } = {}
local COOLDOWN = 0.5
local function isRateLimited(player: Player): boolean
local now = os.clock()
local last = lastAction[player] or 0
if now - last < COOLDOWN then
return true
end
lastAction[player] = now
return false
end
See references/security.md for comprehensive security patterns.
-- Using wait() - use task.wait()
wait(1) -- BAD
task.wait(1) -- GOOD
-- spawn() - use task.spawn()
spawn(fn) -- BAD
task.spawn(fn) -- GOOD
-- delay() - use task.delay()
delay(1, fn) -- BAD
task.delay(1, fn) -- GOOD
-- Polling when events exist
while true do
if something then break end
task.wait()
end
-- GOOD: Use events/signals instead
-- String concatenation in loops
local s = ""
for i = 1, 1000 do
s = s .. tostring(i) -- O(n²)
end
-- GOOD: Use table.concat
-- FindFirstChild chains
workspace.Folder.SubFolder.Part -- Errors if missing
-- GOOD: Safe navigation
local folder = workspace:FindFirstChild("Folder")
local part = folder and folder:FindFirstChild("SubFolder")
and folder.SubFolder:FindFirstChild("Part")
-- Generalized iteration
for _, v in ipairs(array) do end -- OLD
for _, v in array do end -- MODERN (Luau)
-- If expressions
local x = if condition then a else b -- Clean ternary
-- Continue in loops
for _, item in items do
if not item.valid then continue end
process(item)
end
-- Optional chaining with and
local name = player and player.Character and player.Character.Name
src/
├── Server/
│ ├── init.server.luau # Bootstrap
│ ├── Services/ # Game services
│ │ ├── DataService.luau
│ │ └── CombatService.luau
│ └── Components/ # Server components
├── Client/
│ ├── init.client.luau # Bootstrap
│ ├── Controllers/ # Client controllers
│ └── UI/ # UI components
├── Shared/
│ ├── Types.luau # Shared type definitions
│ ├── Constants.luau # Shared constants
│ └── Util/ # Shared utilities
└── Packages/ # Wally packages
| Do | Don't |
|---|---|
task.wait() | wait() |
task.spawn() | spawn() |
task.delay() | delay() |
for _, v in t | for _, v in pairs(t) |
| Validate on server | Trust client data |
| Use types | Use any everywhere |
| Disconnect events | Leave connections dangling |
| Use constants | Magic numbers/strings |
| Early return | Deep nesting |
| Small functions | 200+ line functions |
juliusbrussee/caveman
mattpocock/skills
shadcn/improve
obra/superpowers
forrestchang/andrej-karpathy-skills
vercel-labs/skills