Skip to content

2. Getting Started

Guide Home | Previous: 1. Overview | Next: 3. Core Concepts

View in another language — C#/.NET · C++ · Java · Kotlin · Node/TypeScript

The document that owns this chapter's contract — none. This is a walkthrough for installing and confirming your first working setup.

First install the package and run a minimal example of two processes calling each other (§1-§2), then follow the actual TicTacToe sample through the flow of creating one room (§3-§11).

1. Installation

Get it from npm. The minimal combination needed to build one server consists of these two packages.

npm install @zlink-systems/framework   # The contract and runtime
npm install @zlink-systems/nestjs      # DI/module registration

Packages to add when you need them:

Package When to add it
@zlink-systems/framework-locations-redis When using the Redis location store for auto-connect (10-location)
@zlink-systems/framework-codec-protobuf · -codec-msgpack To use instead of the default JSON codec (05-channel-messaging §7)
@zlink-systems/stream-connector When building an external client (a game client, mobile) (09-stream)
@zlink-systems/http-client When the server calls out over HTTP (HTTP Client guide)

Node.js 20 or later is required.

The license differs by layer — core/binding is MPL-2.0, framework is FSL-1.1-ALv2, and @zlink-systems/http-client is Apache-2.0. There's no cost to building and selling a service (17-alternative §7).

2. A Minimal Example — Two Processes Calling Each Other

With no location store and no Redis, try one request/reply over a manual connection with the endpoint specified directly. This confirms that installation is complete.

The shared contract. Both processes reference the same record.

export interface Hello { readonly name: string; }
export interface Greeting { readonly text: string; }

The server process. Owns the greeting channel and registers a handler.

@Module({
  imports: [
    ZLinkModule.forRootFactory({
      useFactory: () => {
        const builder = zlinkFramework();

        // Names the mesh.
        const mesh = builder.addRouteMesh('services')
          // Its own endpoint for other processes to connect to.
          .listen('tcp://0.0.0.0:7101');
        // This process handles "greeting".
        mesh.channel('greeting').server()
          .addRequestHandler(PacketNames.hello, HelloHandler);

        return builder.build();
      }
    }),
    // Gathers handlers as providers.
    zlinkModule(__dirname, { })
  ]
})
export class ServerModule {}

// A handler that processes one request.
@zlinkRequestHandler('greeting', PacketNames.hello)
export class HelloHandler implements ZLinkRequestHandler<Hello, Greeting> {

  async handle(request: Hello): Promise<Greeting> {
    return { text: `hello, ${request.name}` };
  }
}

The client process. Joins the same mesh and calls greeting.

ZLinkModule.forRootFactory({
  useFactory: () => {
    const builder = zlinkFramework();
    // It also needs its own endpoint.
    const mesh = builder.addRouteMesh('services').listen('tcp://0.0.0.0:7102');
    // The call-only side is client.
    mesh.channel('greeting').client();
    // Manual connection — write the server endpoint directly.
    mesh.peerConnections().connect('tcp://127.0.0.1:7101');
    return builder.build();
  }
})

@Controller()
export class HelloController {
  constructor(@Inject(ZLINK_ROUTE_CLIENT) private readonly route: ZLinkRouteClient) {}

  @Get('/hello/:name')
  async hello(@Param('name') name: string): Promise<string> {
    // The target is just one ChannelName. Which node handles it isn't specified.
    const reply = await this.route
      .requestToChannel('greeting', { name })
      .submit<Greeting>();
    return reply.text;
  }
}

Start the server first, then the client, and call curl http://localhost:5000/hello/world — it returns hello, world.

This confirms three things: the package is wired up, the two processes are connected through the mesh, and the call was routed by logical name (greeting) alone. This example has no Redis and no location store. For the calling code to stay the same as servers scale up and down, you need auto-connect, which is covered by 10-location.

3. TicTacToe — The Flow of Creating One Room

From here on, we'll use an actual sample. The API server doesn't pick a specific Play node — it only passes the room's stable type and its initial settings. The Framework selects one of the Object Servers that registered that type, and issues a globally unique SpotId.

3.1 Execution Flow

↗ View larger

The API code never carries the Play node's NodeRid or endpoint. The same creation code is used even as Play nodes are added or replaced.

3.2 Sample Locations

What to check File
Full run samples/TicTacToe.Ts/run_sample.sh
API entry point samples/TicTacToe.Ts/Server/Api/main.ts
Play entry point samples/TicTacToe.Ts/Server/Play/main.ts
HTTP handler samples/TicTacToe.Ts/Server/Api/Handlers/create-game-http-handler.ts
Game Spot samples/TicTacToe.Ts/Server/Play/Infrastructure/ZLink/Spots/TicTacToeGameSpot/tictactoe-game-spot.ts
Shared messages samples/TicTacToe.Ts/Shared/Contracts/messages.ts

The table's relative paths are rooted at framework/languages/node.

4. API Server Configuration

The API server registers a Location Store and an Object Client role. The Object Client role is used to create or call Actors and Spots on another Object Server.

useFactory: () => {
  const builder = zlinkFramework();
  // Registers a shared Store so every process queries the same location information.
  builder.addLocationStore(new ZLinkRedisLocationStore({
    url: settings.redisEndpoint,
    keyPrefix: settings.redisKeyPrefix
  }));

  const mesh = builder.addRouteMesh(SampleNodes.mesh)
    .listen(settings.meshEndpoint)
    .setRoutingIdPrefix('tictactoe-api');

  // The API process doesn't hold any Object — it only initiates remote Object calls.
  mesh.objects().client();
  return builder.build();
}

The sample reads the peer endpoint from a config file for reproducible local runs. This endpoint only sets up the connection — it doesn't specify which Play node the new Game Spot gets placed on.

5. Creating a Spot from an HTTP Request

The HTTP handler uses the spot manager it received through DI.

// The HTTP handler uses the injected spot manager.
@Post('/games')
async create(@Body() request: CreateGameHttpReq): Promise<CreateGameHttpRes> {
  const gameName = request.gameName?.trim() || SampleDefaults.gameName;

  const created = await this.spots
    // A node that provides this stable type becomes a candidate.
    .create(SampleTypes.gameSpot)
    // Selects the RouteMesh to create the Object on.
    .inMesh(SampleNodes.mesh)
    .request(tictactoeGameCreateReq(
      gameName,
      // The initial settings passed to the new Spot's onCreate.
      SampleDefaults.requiredLevel))
    .submit();

  return createGameHttpRes(
    // Uses the Framework-issued SpotId as the room id.
    created.spot.spotId,
    this.settings.playEndpoints,
    this.settings.playNodes,
    gameName,
    SampleDefaults.requiredLevel);
}

Use create to create a new User Spot where the caller doesn't decide the SpotId. To look up or create the same SpotId again, use GetOrCreate(spotId, spotType).

6. Registering a Stable Type on the Play Server

The Framework considers only Serving Object Servers that have registered the requested stable type as creation candidates. The Play server registers the TicTacToeGame factory as follows.

const mesh = builder.addRouteMesh(SampleNodes.mesh)
  .listen(settings.meshEndpoint)
  .setRoutingIdPrefix('tictactoe-play');

mesh.objects().server()
  .addSpotFactory(
    // The same stable type the API passed to create.
    SampleTypes.gameSpot,
    TicTacToeGame,
    factory => factory.disableRelocation());

The sample does not define a contract for preferring a specific Play node or placing by NodeRid. The Framework and Location Store decide the placement candidate and capacity.

7. Validating the Initial Settings

The selected Play node creates the Spot, then hands the initial request to onCreate. The Spot validates the settings and returns whether it accepts creation.

async onCreate(request: ZLinkMessage): Promise<ZLinkSpotCreateResponse> {
  const settings = request.decode<TicTacToeGameCreateReq>(Object as never);

  if (!settings.gameName?.trim())
    return ZLinkSpotCreateResponse.reject('GameName is required.');

  this.gameName = settings.gameName;
  this.requiredLevel = settings.requiredLevel;

  // Only after accept is this Spot published as Ready in the Location Store.
  return ZLinkSpotCreateResponse.accept();
}

If creation is rejected, that reservation is never published as a Ready Spot. The caller receives a typed failure as the completion result.

8. What the ClientServer Channel Is For

TicTacToe's tictactoe.api ClientServer channel is used when a Play session requests user authentication from the API server. It isn't used for Game Spot creation.

// API process: handles the authentication request.
builder.addClientServerChannel(SampleChannels.api)
  .server()
  .listen()
  .addRequestHandler(PacketNames.authenticatePlayerReq, AuthenticatePlayerHandler);

// Play process: sends the authentication request.
builder.addClientServerChannel(SampleChannels.api).client();

Object creation and a ClientServer call are different features. No dedicated room-creation channel or CreateGameHandler is added.

9. Build and Run

# Build the sample first.
npm --prefix framework/languages/node run build

# Prepares Redis and 4 processes, and verifies the whole scenario.
framework/languages/node/samples/TicTacToe.Ts/run_sample.sh

The runner runs 2 APIs and 2 Plays. After creating a Game Spot, it verifies that participants connected to different Play endpoints join the same room, then checks game messaging and end-of-game cleanup.

10. What to Check When It Fails

Symptom What to check
No creation candidate Check whether the Play process registered an Object Server and the GameSpot stable type on the same MeshName.
Startup fails Check the Redis connection, MeshName, listen endpoint, and any duplicate-registration error.
Creation is rejected Check the initial settings onCreate received and the reject reason.
The client can't join the room Check whether the HTTP response's RoomId was passed to the Actor join request as-is.

The next chapters each explain the role of the channel, Spot, Actor, Stream, and Location Store used here.