Skip to Content
PlatformsRobloxRecipe: Server & Client APIs

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

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).Server

With 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) end

Consumers 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:

  1. 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 =.
  2. Shared code needs a third package (mylibrary-shared), and now a bugfix in shared code is a three-package release train.
  3. 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.

Last updated on