Guide
MVC Pattern
RoExpress maps onto MVC cleanly: models own data, controllers own routes, the Bridge decouples them without shared imports.
Folder layout
ServerScriptService/
Server/
Models/
PlayerModel.luau
Controllers/
ShopController.luau
App.luau ← creates App, wires controllers
PlayerModel.luau
local Players = game:GetService("Players")
local DS = game:GetService("DataStoreService"):GetDataStore("v1")
local bridge = RoExpress("Bridge")
local cache = {}
local M = {}
function M.Get(userId) return cache[userId] end
function M.AddCoins(userId, amount)
local d = cache[userId]; if not d then return end
d.coins += amount
bridge.Fire("player.coins.changed", { userId = userId, coins = d.coins })
end
Players.PlayerAdded:Connect(function(p)
local ok, d = pcall(function() return DS:GetAsync(p.UserId) end)
cache[p.UserId] = ok and d or { coins = 0, level = 1 }
bridge.Fire("player.loaded", p)
end)
Players.PlayerRemoving:Connect(function(p)
pcall(function() DS:SetAsync(p.UserId, cache[p.UserId]) end)
cache[p.UserId] = nil
end)
return M
ShopController.luau
local PlayerModel = require(Models.PlayerModel)
local catalogue = { sword = { price = 50 }, shield = { price = 75 } }
local M = {}
function M.Register(app)
app:Get("shop/catalogue", function(P, _, req, res)
res:Send(catalogue)
end)
app:Post("shop/buy", function(Player, _, req, res)
local item = catalogue[req.data and req.data.id]
if not item then res:Status(404):Error("Not found"); return end
local d = PlayerModel.Get(Player.UserId)
if d.coins < item.price then res:Status(400):Error("Insufficient"); return end
PlayerModel.AddCoins(Player.UserId, -item.price)
res:Send({ ok = true, remaining = PlayerModel.Get(Player.UserId).coins })
end)
end
return M
App.luau | wire it together
local app = RoExpress("App")
local ShopController = require(Controllers.ShopController)
ShopController.Register(app)
-- push data once model fires loaded
local bridge = RoExpress("Bridge")
bridge.Bind("player.loaded", function(player)
app:Push(player, "player.data", PlayerModel.Get(player.UserId))
end)
Decoupling rule. Controllers never import each other | only models and Bridge. This means you can add a new controller without touching any existing file.
See also
App · Bridge | decoupling event bus · Server Push · Player Data | flat version of this pattern · Shop | flat version