{"id":"unofficialrobloxtutor/roexpress","name":"roexpress","scope":"unofficialrobloxtutor","platform":"roblox","description":"Express.js-style structured networking for Roblox. One remote, typed routes, server push, exploit detection, named ports, Bridge v2, Maid, Debounce, and full Luau typed call form.","version":"2.5.0","latest":"2.5.0","versions":["2.0.0","2.1.0","2.2.0","2.2.1","2.2.2","2.2.3","2.4.0","2.5.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"904291aa09e0b07631a43856a2b5c7d206af0fecd8688bca7e03eb947f471ae8","likes":0,"downloads":0,"install":"forest install unofficialrobloxtutor/roexpress","url":"https://forest.dev/p/roblox/unofficialrobloxtutor/roexpress","files":"https://api.forest.dev/ai/package/roblox/unofficialrobloxtutor/roexpress/files","readme":"# RoExpress\n\nA type-safe, rate-limited, Express.js-style networking framework for Roblox.\n\n> **Author:** DeathToTheStadium\n> **Version:** 2.5.0\n> **License:** MIT\n> **Docs:** https://roexpress.dev\n\n---\n\n## Overview\n\nRoExpress replaces scattered RemoteEvents with a single disciplined pipeline. One reliable RemoteEvent handles all request/response traffic. One UnreliableRemoteEvent handles all broadcasts. Every request is automatically versioned, rate-limited, routed, and optionally compressed.\n\n```\nTypical Roblox game:\n    RemoteEvent1 → handler\n    RemoteEvent2 → handler\n    RemoteEvent3 → handler\n    ... one remote per action, no structure, no rate limiting, no security\n\nRoExpress:\n    One reliable remote   → full request/response pipeline + server push\n    One unreliable remote → full broadcast pipeline\n    Named ports           → isolated pipelines for separate traffic domains\n    Stream remotes        → dedicated 60hz binary FPS streaming\n```\n\n---\n\n## Module Tree\n\n```\nRoExpress\n├── App            server  — routing, middleware, server push\n│   ├── Router             — typed params, wildcards, globs, constraints\n│   └── TokenBucket        — per-instance rate limiter\n├── Network        client  — request/response, Promise API\n├── Broadcast      server  — unreliable fire-and-forget\n├── Listener       client  — broadcast + reliable push\n├── Bridge         shared  — internal event bus\n├── Tamper         server  — exploit detection\n├── Codec          shared  — LZ77 + LZH (Deflate) compression (folder)\n│   ├── LZ77               — sliding-window byte compression\n│   └── LZH                — Deflate-compatible entropy coding\n├── Port           server  — named isolated pipelines\n├── Stream         shared  — schema-defined typed binary channels (folder)\n│   ├── Types              — 20 built-in wire types + custom extension\n│   ├── Schema             — compile-time field offsets, pack/unpack, delta\n│   └── Channel            — channel instance, rate limiting, sequence numbers\n├── TypeCoercer    shared  — type serialisation utility\n├── Promise        client  — chainable async Network API\n├── TokenBucket    shared  — rate limiter (used internally)\n└── Base64         shared  — encode/decode utility\n```\n\n---\n\n## Installation\n\n### Wally (recommended)\n\n```toml\n[dependencies]\nRoExpress = \"unofficialrobloxtutor/roexpress@2.5.0\"\n```\n\n```bash\nwally install\n```\n\n```lua\nlocal RoExpress = require(game.ReplicatedStorage.Packages.RoExpress)\n```\n\n### Creator Store\n\nGet it from the [Roblox Creator Store](https://create.roblox.com/store/asset/94926286357335) and drop the ModuleScript into `ReplicatedStorage`.\n\n### Manual / GitHub\n\nClone or download from [GitHub](https://github.com/unofficialrobloxtutor/RoExpress) and place in `ReplicatedStorage`:\n\n```\nReplicatedStorage\n└── RoExpress          ← root ModuleScript (init.luau)\n    ├── App\n    ├── Network\n    ├── Broadcast\n    ├── Listener\n    ├── Router\n    ├── Codec\n    ├── Bridge\n    ├── Tamper\n    ├── Port\n    ├── Stream\n    ├── TypeCoercer\n    ├── Promise\n    ├── TokenBucket\n    ├── Version\n    └── Base64\n```\n\nRoExpress creates its own RemoteEvents automatically — you don't touch them.\n\n---\n\n## Quick Start\n\n### Server\n\n```lua\nlocal RoExpress = require(game.ReplicatedStorage.Modules.Libraries.RoExpress)\nlocal app       = RoExpress(\"App\")\nlocal broadcast = RoExpress(\"Broadcast\")\nlocal bridge    = RoExpress(\"Bridge\")\n\n-- middleware — runs before every request\napp:Use(\"logger\", function(Player, Payload)\n    print(Player.Name, Payload.method, Payload.route)\nend)\n\n-- typed param — req.params.userId is already a Lua number\napp:Get(\"player/:userId=number\", function(req, res)\n    res:Send({ userId = req.params.userId })\nend)\n\n-- update a record — returns status only, no body\napp:Put(\"player/:userId=number/name\", function(req, res)\n    -- update logic here\n    res:Status(200):Send(true)\nend)\n\n-- delete a record — returns status only, no body\napp:Delete(\"player/:userId=number\", function(req, res)\n    -- delete logic here\n    res:Status(204):Send()\nend)\n\n-- compressed response — best on large tables (>2kb)\napp:Get(\"feed/all\", handler, { compress = true })\n\n-- server push — reliable, no client request needed\napp:PushAll(\"roundEnd\", { winner = \"PlayerName\" })\n\n-- internal bus — fire to other server modules\nbridge.Fire(\"playerJoined\", { player = Player })\n```\n\n### Client\n\n```lua\nlocal RoExpress = require(game.ReplicatedStorage.Modules.Libraries.RoExpress)\nlocal network   = RoExpress(\"Network\")\nlocal listener  = RoExpress(\"Listener\")\n\n-- GET with callback\nnetwork:Get(\"player/123\", nil, function(res)\n    if res.type == \"error\" then return end\n    print(res.data.userId)\nend)\n\n-- PUT — update, expects status only back\nnetwork:Put(\"player/123/name\", { name = \"NewName\" }, function(res)\n    print(res.status) -- 200\nend)\n\n-- DELETE — remove, expects status only back\nnetwork:Delete(\"player/123\", nil, function(res)\n    print(res.status) -- 204\nend)\n\n-- GET with Promise\nnetwork:GetAsync(\"player/123\")\n    :Then(function(res) print(res.data.userId) end)\n    :Catch(function(err) warn(err.message) end)\n\n-- listen to both reliable push and unreliable broadcast\nlistener:On(\"roundEnd\", function(data)\n    print(\"Winner:\", data.winner)\nend)\n```\n\n---\n\n## Context Access\n\n| Call | Context | Returns |\n|------|---------|---------|\n| `RoExpress(\"App\")` | Server only | App instance |\n| `RoExpress(\"Network\")` | Client only | Network instance |\n| `RoExpress(\"Broadcast\")` | Server only | Broadcast instance |\n| `RoExpress(\"Listener\")` | Client only | Listener instance |\n| `RoExpress(\"Bridge\")` | Both | Shared singleton event bus |\n| `RoExpress(\"Tamper\")` | Server only | Exploit detection singleton |\n| `RoExpress(\"Stream\")` | Both | Schema-defined typed binary channel singleton |\n| `RoExpress(\"TypeCoercer\")` | Both | Type serialisation utility |\n| `RoExpress(\"Promise\")` | Client only | Promise factory |\n| `RoExpress(\"Base64\")` | Both | Base64 utility |\n\nCalling a server-only module on the client (or vice versa) throws an assertion with a clear context message.\n\n---\n\n## API Reference\n\n### App (Server)\n\n```lua\nlocal app = RoExpress(\"App\")\n```\n\n#### Routing\n\n```lua\napp:Get(route, handler, options?)\napp:Post(route, handler, options?)\napp:Put(route, handler, options?)\napp:Delete(route, handler, options?)\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `route` | `string` | Supports typed params, wildcards, globs, inline constraints |\n| `handler` | `function` | See handler signatures below |\n| `options.compress` | `boolean?` | Enable LZ77 compression on the response (GET/POST only) |\n\n#### Handler Signatures\n\nTwo calling conventions are supported. RoExpress detects which one to use automatically based on the number of parameters.\n\n```lua\n-- Modern (recommended)\nfunction(req, res) end\n\n-- Legacy\nfunction(Player, Payload, req, res) end\n```\n\nIn the modern signature `req.player` and `req.raw` are populated automatically. In the legacy signature `Player` and `Payload` are passed directly as the first two arguments.\n\n#### Method Conventions\n\n| Method | Has body? | Response | Enforced |\n|--------|-----------|----------|----------|\n| `GET` | optional | data | — |\n| `POST` | yes | encoded data (Base64 / Deflate) | — |\n| `PUT` | yes | `boolean?` / `nil` only | table body is warned + stripped |\n| `DELETE` | optional | `boolean?` / `nil` only | table body is warned + stripped |\n\n#### Route Syntax\n\n| Syntax | Example | Description |\n|--------|---------|-------------|\n| Literal | `player/coins` | Exact match |\n| Plain param | `:name` | Any segment → string |\n| Typed param | `:id=number` | Coerced to declared type |\n| Constrained | `:id(\\d+)` | Must match Lua pattern |\n| Wildcard | `*` | One segment → `req.captures[n]` |\n| Glob | `**` | Zero-or-more segments → `req.captures[n]` as table |\n\n#### Supported Param Types\n\n`string` · `number` · `int` · `boolean` · `vector2` · `vector3` · `color3` · `cframe` · `Enum.TypeName` · `Instance`\n\n#### req Object\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `req.params` | `{[string]: any}` | Named params, coerced to declared type |\n| `req.captures` | `{any}` | Positional wildcard/glob captures |\n| `req.query` | `{[string]: string}` | Query string params e.g. `?limit=5` |\n| `req.data` | `any` | Raw payload from client |\n| `req.player` | `Player?` | The requesting player (modern signature only) |\n| `req.raw` | `Payload?` | Full raw payload table (modern signature only) |\n\n#### res Object\n\n| Method | Description |\n|--------|-------------|\n| `res:Send(data?)` | Send success response. PUT/DELETE accept `boolean?` or `nil` only — passing a table is warned and stripped. Callable once. |\n| `res:Error(message)` | Send error response. Callable once. |\n| `res:Status(code)` | Set status code. Chainable. |\n\n#### Middleware\n\n```lua\napp:Use(id, fn)   -- register — return false to block (403), throw for 500\napp:Unuse(id)     -- remove by id\n```\n\n#### Server Push\n\n```lua\napp:Push(player, event, data)        -- reliable push to one player\napp:PushAll(event, data)             -- reliable push to all players\napp:PushTo(players, event, data)     -- reliable push to a list\n```\n\nReceived on the client via `listener:On(event, handler)`.\n\n#### Named Ports\n\n```lua\napp:Listen(name, callback, settings?)  -- create an isolated pipeline\napp:GetPort(name)                      -- retrieve a port by name\n```\n\n```lua\napp:Listen(\"combat\", function(port)\n    port:Post(\"gun/fire/:damage=number\", handler)\nend, { Max = 30, Refill = 10 })\n\n-- client\nlocal combat = RoExpress(\"Network\", \"combat\")\n```\n\n#### Other\n\n```lua\napp:OnParamError(fn)   -- custom typed param failure handler\napp.TokenBucket        -- direct access to the rate limiter\napp:Destroy()\n```\n\n---\n\n### Network (Client)\n\n```lua\nlocal network = RoExpress(\"Network\")\n```\n\n#### Callbacks\n\n```lua\nnetwork:Get(route, data?, callback?, timeout?, retries?)\nnetwork:Post(route, data, callback?, timeout?, retries?)\nnetwork:Put(route, data, callback?, timeout?, retries?)\nnetwork:Delete(route, data?, callback?, timeout?, retries?)\n```\n\nCallbacks and timeout are optional. Omit the callback to block the current thread until the response arrives. All return a `requestId`.\n\n#### Promises\n\n```lua\nnetwork:GetAsync(route, data?, timeout?, retries?)    -- returns Promise\nnetwork:PostAsync(route, data, timeout?, retries?)    -- returns Promise\nnetwork:PutAsync(route, data, timeout?, retries?)     -- returns Promise\nnetwork:DeleteAsync(route, data?, timeout?, retries?) -- returns Promise\n```\n\n```lua\nnetwork:GetAsync(\"leaderboard/top\")\n    :Then(function(res) return res.data.entries end)\n    :Then(function(entries) UI:Load(entries) end)\n    :Catch(function(err) warn(err.message) end)\n    :Finally(function() UI:HideLoader() end)\n```\n\n#### NetworkResponse\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `res.type` | `\"response\" \\| \"error\"` | Whether the request succeeded |\n| `res.status` | `number` | HTTP-style status code |\n| `res.data` | `any?` | Payload — decompressed automatically if compressed |\n| `res.message` | `string?` | Error message (nil on success) |\n| `res.compressed` | `boolean?` | True if the payload was Deflate-compressed |\n\n#### Other\n\n```lua\nnetwork:Configure({ maxRetries = 3, backoff = 1 })  -- global retry config (default: 3 retries, 1s backoff)\nnetwork:Cancel(requestId)                            -- cancel pending request, returns boolean\nnetwork:Destroy()\n```\n\nRetries use exponential backoff starting at `backoff` seconds (1s → 2s → 4s). Retryable status codes: `408`, `429`, `500`. Non-retryable: `400`, `403`, `404`. The `retries?` parameter on each method overrides the global config per-request.\n\n---\n\n### Broadcast (Server)\n\n```lua\nlocal broadcast = RoExpress(\"Broadcast\")\n\nbroadcast:Emit(event, player, data)\nbroadcast:EmitAll(event, data)\nbroadcast:EmitTo(event, targets, data)\nbroadcast:Destroy()\n```\n\nUses `UnreliableRemoteEvent`. Subject to per-event and per-player rate limiting. Data cap: 900 bytes.\n\n---\n\n### Listener (Client)\n\n```lua\nlocal listener = RoExpress(\"Listener\")\n\nlistener:On(event, handler)     -- persistent subscription\nlistener:Once(event, handler)   -- fires once then unsubscribes\nlistener:Off(event)             -- remove all handlers for event\nlistener:Use(id, fn)            -- middleware before every handler\nlistener:Unuse(id)\nlistener:Destroy()\n```\n\nHandles both unreliable broadcast and reliable server push through one API.\n\n---\n\n### Bridge (Shared)\n\n```lua\nlocal bridge = RoExpress(\"Bridge\")  -- same instance everywhere in this context\n\nbridge.On(name, handler)            -- subscribe; returns Connection\nbridge.Once(name, handler)          -- fires once then auto-disconnects; returns Connection\nbridge.Emit(name, data?)            -- fire to all handlers on the channel\nbridge.Has(name)                    -- returns true if channel has handlers\nbridge.Clear(name?)                 -- clear one channel or all channels\nbridge.Destroy()                    -- full teardown\n\n-- connections — disconnect a specific handler without clearing the whole channel\nlocal conn = bridge.On(\"kill\", handler)\nconn:Disconnect()\n\n-- yieldable variants\nbridge.Wait(name, timeout?)                           -- yields until channel fires\nbridge.WaitUntil(name, predicate, timeout?)           -- yields until predicate returns true\nbridge.WaitFirst(names, timeout?)                     -- yields until any channel fires\n```\n\nBridge is purely in-process — it does not cross the client/server boundary.\n\n---\n\n### Tamper (Server)\n\n```lua\nlocal tamper = RoExpress(\"Tamper\")\n\ntamper.On(handler)                              -- subscribe to detection reports\ntamper.AutoKick(threshold, reason?)             -- opt-in auto-kick\ntamper.Strike(player, reason?, route?, evidence?) -- manual strike\ntamper.GetReport(player)                        -- full player record\ntamper.GetStrikes(player)                       -- strike count\ntamper.ClearStrikes(player)\ntamper.ClearAll()\ntamper.SetThresholds(config)\n```\n\n#### Detection Reasons\n\n| Reason | Tier | Trigger |\n|--------|------|---------|\n| `VERSION_SPOOF` | immediate | Client version mismatch |\n| `MALFORMED_PAYLOAD` | immediate | Payload fails validation |\n| `INVALID_PARAM` | immediate | Typed param coercion fails |\n| `UNKNOWN_ROUTE` | immediate | Route does not exist |\n| `RATE_FLOOD` | pattern | Repeated 429s in window |\n| `ROUTE_SCAN` | pattern | Many distinct unknown routes |\n| `PARAM_FLOOD` | pattern | Repeated param failures on same route |\n| `MANUAL` | immediate | Developer called `tamper.Strike()` |\n\n---\n\n### Codec (Shared)\n\n```lua\nlocal Codec = require(script.Parent.Codec)\n\nCodec.Compress(data)        -- any → LZ77 base64 string  (compat alias)\nCodec.Deflate(data)         -- any → LZH (Deflate) base64 string\nCodec.Inflate(str)          -- base64 string → any  (auto-detects LZ77 vs LZH via magic bytes)\nCodec.Decompress(str)       -- alias for Inflate\nCodec.IsCompressed(str)     -- → boolean\n```\n\nTwo compression algorithms over Roblox's native `buffer` type:\n\n| Algorithm | Method | Best for |\n|-----------|--------|----------|\n| LZ77 | `Codec.Compress` | General repetitive data |\n| LZH (Deflate) | `Codec.Deflate` | Larger payloads, better ratio |\n\nOpt-in per route via `{ compress = true }`. Decompression is automatic on the client — transparent to your callback. `Codec.Decompress` auto-detects which algorithm was used via magic bytes.\n\nTypical savings: 30–60% on JSON. Not worth enabling under ~500 bytes.\n\n---\n\n### Stream (Shared)\n\n```lua\nlocal Stream = RoExpress.Stream\n```\n\nSchema-defined typed binary channels over raw Roblox buffers. No JSON, no Base64. Both server and client define identical channels — no manual numbering, no ordering dependency.\n\nAll channels are multiplexed over two shared remotes (`StreamUnreliable` / `StreamReliable`).\n\n#### Quick Start\n\n```lua\n-- Shared — define the same channels on server and client\nlocal move = Stream.Channel(\"playerMove\", Stream.Schema.New({\n    { \"pos\", ","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/unofficialrobloxtutor/roblox/roexpress/2.5.0/readme"}