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. Forest also has no realm concept, so there are no separate server-only/client-only install targets (details). 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 halves into separate realm-mounted trees (the two-manifest pattern), and a consumer who does that can just as easily mount a single-package library server-side.
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.