Getting Started

RoExpress | the complete networking stack for Roblox

The first Roblox framework that covers every layer of networking in one place: inbound routing with typed params and middleware, reliable server push, tick-rate binary streaming, outbound HTTP to external APIs, built-in exploit detection, per-player rate limiting, and Deflate compression. Stop patching together RemoteEvents. Start with a stack that is already complete.

v2.4 is here. Deflate compression (LZ77 + Huffman), automatic retry with exponential backoff, Stream generic channels, and fully typed GetApp() / GetNetwork() accessors.

Module tree

RoExpress ├── App server | routing, middleware, server push │ ├── Router | typed params, wildcards, globs, constraints │ └── TokenBucket | per-instance rate limiter ├── Network client | request/response, retry, Promise API ├── Broadcast server | unreliable fire-and-forget ├── Listener client | broadcast + reliable push subscriptions ├── Bridge shared | internal server-side event bus ├── Tamper server | passive exploit detection & strike system ├── Codec shared | Deflate compression (LZ77 + Huffman) ├── Port server | named isolated pipelines ├── Stream shared | schema-defined typed binary channels │ ├── Types | 20 built-in wire types + custom extension │ ├── Schema | compile-time offsets, pack/unpack, delta │ └── Channel | instance, rate limiting, sequence numbers ├── TypeCoercer shared | Roblox type serialisation utility ├── Promise client | chainable async Network API ├── Benchmark client | round-trip latency measurement ├── Harpy server | outbound HTTP client (base URL, headers, retry) └── Base64 shared | encode/decode utility

Installation

The fastest way is the Studio command bar | paste one line and you're done. Full install guide →

local H=game:GetService"HttpService";loadstring(H:GetAsync"https://raw.githubusercontent.com/unofficialrobloxtutor/RoExpress/main/install.lua")()

Or with Wally:

[dependencies]
RoExpress = "unofficialrobloxtutor/roexpress@2.4.0"
wally install

Quick start

Server

local RoExpress = require(game.ReplicatedStorage.RoExpress)
local app       = RoExpress.GetApp()

-- global middleware | runs before every handler
app:Use("logger", function(Player, Payload)
    print(Player.Name, Payload.method, Payload.route)
end)

-- typed :userId param auto-converted to number
app:Get("player/:userId=number", function(req, res)
    res:Send({ userId = req.params.userId })
end)

app:Put("player/:userId=number/name", function(req, res)
    res:Status(200):Send()
end)

-- push to all connected clients reliably
app:PushAll("round.end", { winner = "PlayerName" })

Client

local RoExpress = require(game.ReplicatedStorage.RoExpress)
local network   = RoExpress.GetNetwork()
local listener  = RoExpress("Listener")

-- callback style
network:Get("player/123", nil, function(res)
    print(res.data.userId)
end)

-- promise style (v2.2+)
network:GetAsync("player/123")
    :Then(function(res) return res.data end)
    :Catch(function(err) warn(err.message) end)

-- reliable push subscription
listener:On("round.end", function(data)
    print("Winner:", data.winner)
end)

Context accessors

There are three access forms. Use typed accessors for App and Network | Luau infers the full type automatically so autocomplete and type checking work without an annotation. The call form (RoExpress("Name")) returns any. Property access (RoExpress.Name) is used for utility modules that don't need instantiation.

CallContextReturns
Typed accessors | use these
RoExpress.GetApp() ServerApp | fully typed, autocomplete works
RoExpress.GetNetwork() ClientNetwork | fully typed, autocomplete works
Named ports | client side
RoExpress("Network", "portName") ClientNetwork wired to the named port's RemoteEvent
RoExpress("Listener", "portName")ClientListener wired to the named port's RemoteEvent
Other modules
RoExpress("Broadcast") ServerBroadcast instance (returns any)
RoExpress("Listener") ClientListener wired to the main channel
RoExpress.Stream Both Stream factory
RoExpress.Bridge Both Bridge singleton
RoExpress.Codec Both Codec module
RoExpress.Harpy ServerHarpy factory (call .New(config))
RoExpress.Tamper ServerTamper singleton

Calling a server-only module on the client (or vice versa) throws an assertion with the context name. Port names on the server side are ignored | ports are created via app:Listen(), not via the accessor.

Request pipeline

Every incoming request passes through the same stages in order:

#StageRejects with
1Version check | Tamper discards packets with a mismatched framework versionsilent drop
2TokenBucket | per-player rate limiter consumes one token429
3Payload validation | method, route, and body shape checked400 / Tamper strike
4Global middleware | each registered handler runs in order; return false stops the chain403
5Route match | Router finds the first matching pattern and extracts typed params404
6Route middleware | any :Use scoped to this route403
7Handler | your code runs; res:Send() or res:Error() fires the response500 on unhandled error

Version history

VersionStatusWhat shipped
2.4.0currentDeflate compression · Automatic retry · Stream generic channels · Typed accessors
2.3.0shippedPUT & DELETE · Compact handler args · Codec LZ77 upgrade · TypeCoercer: UDim, Rect, UDim2
2.2.3shippedIssue #6 fix | listener:Once race condition under rapid events
2.2.0shippedTypeCoercer · Promise · Server Push · Tamper · TokenBucket Grant
2.1.0shippedStream v2 · Named Ports · Benchmark · Bridge.BindOnce
2.0.0shippedFull rewrite | App, Router, Network, Listener, Bridge, Codec, Broadcast

Next: YouTube gun framework showcase, then awesome-roblox submission. See the Roadmap for the full picture.

Explore the docs

A random selection of guides, references, and examples — shuffles on every visit.