Recipe: Publishing a Library with Server and Client APIs
One of the most common shapes for a Roblox library is “a server half and a client half that talk to each other”: networking wrappers, replication systems, data frameworks. This page shows the recommended way to ship that as a Forest package.
One package, one root
Recall the entry point rule: a package has exactly one root module, and whatever it returns is the whole public API. Packages also don’t declare a realm: where a package’s code lives is up to the project that installs it (details), so you can’t mark half of your package server-only. Both facts point at the same design:
Ship one package whose root exposes both surfaces, with shared code as an internal sibling.
- forest.json
- README.md
- LICENSE
- init.luau
- Server.luau
- Client.luau
- Shared.luau
Option A: explicit surfaces (recommended)
The root returns both halves; consumers pick the one that applies:
-- src/init.luau
return {
Server = require(script.Server),
Client = require(script.Client),
}-- a consumer's server script
local MyLibrary = require(Packages.mylibrary).ServerWith this shape, requiring the root loads both modules’ source on both sides. Keep Server.luau free of side effects at require time (connect signals in an :Init()/:Start() method, not at the top level), or use Option B.
Option B: branch on context
The root decides which surface to return based on where it’s running:
-- src/init.luau
local RunService = game:GetService("RunService")
if RunService:IsServer() then
return require(script.Server)
else
return require(script.Client)
endConsumers just require(Packages.mylibrary) and get the right API for their side. The trade-off: the two surfaces can’t have different types visible to tooling at the same time, and code that runs in both contexts (e.g. shared utilities re-exported on both) needs care.
Shared code
Shared.luau is not re-exported. Server and Client require it internally via script.Parent.Shared. Because packages are self-contained folders with relative requires, this works no matter where the consumer mounts the packages tree. Never reach outside your own package folder (e.g. a hardcoded ReplicatedStorage.SharedModules path); that couples your library to one specific project layout and breaks everywhere else.
Why not two packages (mylibrary-server + mylibrary-client)?
You can, but you inherit three problems for zero benefit:
- Lockstep versioning is manual. Dependencies are ranges by default, so nothing forces a consumer’s server half and client half to be the same version unless both you and they pin with
=. - Shared code needs a third package (
mylibrary-shared), and now a bugfix in shared code is a three-package release train. - It buys no isolation on its own. Splitting the package doesn’t keep server code off clients unless the consumer also installs the server half into a server-only mount. Placement is the consumer’s decision either way; your package can’t make it for them.
The single-package shape gives you atomic versioning, private shared internals, and one install command for your users.
Remember: wherever the consumer mounts Packages/ (commonly ReplicatedStorage), the source of the whole package replicates there, including your Server module’s source in most setups. Server-side secrets don’t belong in package code.