Getting Started

Module Accessors

Three patterns for accessing RoExpress modules. As of v2.5, all three are fully typed — Luau infers method lists and return types without any manual annotation.

1  |  Call form  RoExpress("ModuleName")

The primary way to get a constructed module instance. The root module is callable via a typed __call overload — Luau resolves the correct return type from the string literal you pass.

-- Server script
local RoExpress = require(game.ReplicatedStorage.RoExpress)

local app       = RoExpress("App")        -- RoExpress.App
local broadcast = RoExpress("Broadcast")  -- RoExpress.Broadcast
local maid      = RoExpress("Maid")       -- RoExpress.Maid (fresh instance)
-- Client script
local network  = RoExpress("Network")   -- RoExpress.Network
local listener = RoExpress("Listener")  -- RoExpress.Listener

Autocomplete suggests every valid module name as soon as you open the string. The return type narrows to the correct type for that module — RoExpress("App") returns RoExpress.App, RoExpress("Network") returns RoExpress.Network, and so on. All methods and their parameters are available immediately.

local app = RoExpress("App")

app:Get("player/:id=number", function(req, res)
    -- req and res are both fully typed — no annotation needed
    res:Send({ userId = req.params.id })
end)
All results are cached. RoExpress("App") called from two different scripts returns the exact same instance. Modules are only constructed once.

2  |  Property access  RoExpress.ModuleName

Used for utility modules and singleton references that are not constructed — the property is the module table itself. These are lazily loaded on first access and cached immediately.

-- Both contexts
local Bridge      = RoExpress.Bridge       -- event bus singleton
local Hook        = RoExpress.Hook         -- Roblox signal hooks
local Debounce    = RoExpress.Debounce     -- cooldown wrappers
local Codec       = RoExpress.Codec        -- compression
local Base64      = RoExpress.Base64       -- encode / decode
local Promise     = RoExpress.Promise      -- async utilities
local TypeCoercer = RoExpress.TypeCoercer  -- type serialisation

-- Server only
local RTTP   = RoExpress.RTTP     -- outbound HTTP factory
local Tamper = RoExpress.Tamper   -- exploit detection
local Cross  = RoExpress.Cross    -- cross-server messaging

RTTP is a factory — access it as a property, then call .New(config) to create an instance:

local RTTP    = RoExpress.RTTP
local webhook = RTTP.New({ base = WEBHOOK_URL, retries = 3 })
Modules under property access are loaded lazily — requiring RoExpress does not load Bridge, Codec, or any other utility until the first time you access it. Only Version and Types are loaded eagerly on require.

3  |  Named port  RoExpress("Network", "portName")

Client-only. Passing a second argument returns a Network or Listener wired to that port's dedicated RemoteEvent rather than the main channel. The port must already exist on the server via app:Listen().

-- Client: connect to the "combat" port
local combatNet      = RoExpress("Network",  "combat")  -- RoExpress.Network
local combatListener = RoExpress("Listener", "combat")  -- RoExpress.Listener

combatNet:Post("shoot/:targetId=number", nil, function(res)
    showHit(res.data)
end)

combatListener:On("hit.confirmed", function(data)
    print(data.damage)
end)
-- Server: create the port (not via accessor)
local app = RoExpress("App")

app:Listen("combat", function(port)
    port:Post("shoot/:targetId=number", function(req, res)
        res:Send(handleShoot(req.player, req.params.targetId))
    end)
end)

Port instances are also cached by "ModuleName:portName" key.

Full module reference

ModuleContextAccessReturn type
Server
App ServerRoExpress("App") RoExpress.App
BroadcastServerRoExpress("Broadcast") RoExpress.Broadcast
Tamper ServerRoExpress.Tamper RoExpress.Tamper singleton
RTTP ServerRoExpress.RTTP Factory — call .New(config)
Cross ServerRoExpress.Cross RoExpress.Cross singleton
Port Serverapp:Listen("name", fn) RoExpress.Port — via App only
Client
Network ClientRoExpress("Network") RoExpress.Network
Listener ClientRoExpress("Listener") RoExpress.Listener
BenchmarkClientRoExpress("Benchmark") any
Named ports (client only)
Network on port ClientRoExpress("Network", "name") RoExpress.Network
Listener on portClientRoExpress("Listener", "name") RoExpress.Listener
Both contexts
Bridge BothRoExpress.Bridge RoExpress.Bridge singleton
Hook BothRoExpress.Hook RoExpress.Hook singleton
Maid BothRoExpress("Maid") RoExpress.Maid — fresh instance each call
Debounce BothRoExpress.Debounce RoExpress.Debounce utility
Stream BothRoExpress("Stream") any
Codec BothRoExpress.Codec any
Base64 BothRoExpress.Base64 any
TypeCoercerBothRoExpress.TypeCoercer any
Promise BothRoExpress.Promise RoExpress.Promise factory

How the typing works

The root RoExpress value is typed as an intersection of a property table and an overloaded callable type:

-- Simplified view of what Luau sees after require()
type RoExpressRoot = {
    Bridge   : RoExpress.Bridge,
    Hook     : RoExpress.Hook,
    Debounce : RoExpress.Debounce,
    -- ... all property-access modules
} & (("App")       -> RoExpress.App)
  & (("Network")   -> RoExpress.Network)
  & (("Broadcast") -> RoExpress.Broadcast)
  & (("Maid")      -> RoExpress.Maid)
  -- ... all callable modules

Luau resolves the string literal you pass against the overload list and narrows the return type. Autocomplete for the string argument shows all valid module names. Autocomplete on the returned value shows all methods for that module.

Wrong context errors

Calling a server-only module on the client (or vice versa) throws immediately with a clear message. You will never get a silent nil.

-- In a LocalScript:
local app = RoExpress("App")
-- Error: [RoExpress] 'App' is server-only

See also

App  ·  Network  ·  Maid  ·  Types | all exported Luau types  ·  Request Pipeline