Skip to Content
Game ServerStructured Server Project

Structured Project

For larger server projects that require testing, better organization, and scalability, we provide a structured TypeScript-based approach. This method maintains backward compatibility with the simple server.js approach while offering enhanced development capabilities.

Initialization

IMPORTANT: Always initialize your server project with this command first.

If the server/ directory doesn’t exist in your project, run:

npx -y @agent8/gameserver-node init

This command automatically generates the following project structure:

      • server.ts
      • server.test.ts
    • README.md
    • package.json

After initialization, you can modify the generated files to implement your game logic. The workflow is to initialize first, then continue with development.

Basic Server Implementation

The server/src/server.ts file contains your server logic as an exported class:

server/src/server.ts
export class Server { async ping(): Promise<string> { return 'pong'; } async getMyAccount(): Promise<string> { return $sender.account; } async updateScore(points: number): Promise<number> { const myState = await $global.getMyState(); const newScore = (myState.score || 0) + points; await $global.updateMyState({ score: newScore }); return newScore; } }

The code is functionally identical to server.js, but uses TypeScript and requires an export statement.

Testing

Writing Tests

Tests are defined in server/test/server.test.ts:

server/test/server.test.ts
describe('Server', () => { test('ping returns pong', async (server) => { const result = await server.ping(); expect(result).toBe('pong'); }); test('getMyAccount returns account', async (server) => { const result = await server.getMyAccount(); expect(result).toBeTruthy(); }); test('connect changes user', async (server) => { // Change to different user server.connect({ account: 'user-alice' }); const account = await server.getMyAccount(); expect(account).toBe('user-alice'); }); });

The server parameter provides:

  • server.connect({ account: 'user-id' }) - Switch to a different user context
  • Direct access to all server functions for testing

Running Tests

Run tests from your project directory (not from server/):

npx -y @agent8/gameserver-node test

Development Goal: Always write both server code AND tests. After running tests, check the terminal output to see if they pass or fail, then iterate and fix any issues until all tests pass.

Development Console

For rapid development and debugging, you can use the interactive development console:

npx -y @agent8/gameserver-node dev

This command starts an interactive console where you can directly call your server functions and see the results in real-time. It’s useful for:

  • Testing individual functions without writing formal tests
  • Quick debugging and experimentation
  • Exploring your API during development

Example usage:

> ping() 'pong' > getMyAccount() 'user-123' > updateScore(100) 100

The dev console provides the same environment as your tests, allowing you to switch users with connect({ account: 'user-id' }) and call any server function interactively.

Handler Namespaces

For better code organization, you can separate functionality into different handlers using the @Handler decorator:

server/src/userHandler.ts
@Handler('user') class UserHandler { async getMyAccount(): Promise<string> { return $sender.account; } }

Accessing Namespaced Functions

In tests:

const account = await server.user.getMyAccount();

From client:

const result = await server.remoteFunction('user.getMyAccount');

Beyond the handler namespace system, you have complete freedom to architect your server code however you prefer. You can organize classes, modules, and utilities in any way that suits your project.

Building and Deployment

Building and deployment are automatically handled by the Agent8 platform. You don’t need to manually run build or deploy commands.

When you push your code to the repository or use the platform’s deployment features:

  1. The platform automatically builds your TypeScript server from server/src/ to server/dist/server.js
  2. The platform automatically deploys the built server
⚠️

Deployment Priority:

  • If ./server.js exists in the project root, it takes priority
  • Otherwise, server/dist/server.js is deployed

Important Limitations

Isolated VM Environment

Your server code runs in an isolated-vm environment for security and performance. This comes with certain limitations:

✅ Available

  • Provided contexts: $sender, $global, $room, $asset
  • Pure JavaScript/TypeScript libraries (lodash, date-fns, etc.)
  • Most computation and logic processing

❌ Not Available

  • Node.js built-in modules: fs, http, https, net, child_process, etc.
  • Network request libraries: axios, node-fetch, etc. (with some exceptions)
  • File system access
  • External process execution

The isolated-vm environment ensures consistent performance and security across all game servers, but requires pure JavaScript/TypeScript code without Node.js-specific features.

Workflow Summary

  1. Initialize: npx -y @agent8/gameserver-node init
  2. Develop: Write server logic in server/src/
  3. Test interactively: npx -y @agent8/gameserver-node dev (optional)
  4. Test: npx -y @agent8/gameserver-node test
  5. Build: npx -y @agent8/gameserver-node build (server/dist/server.js)
  6. Deploy: Push to repository - building and deployment are handled automatically by the platform

Migrating from Legacy server.js

If you have an existing server.js file, you can migrate to the Structured Project approach:

Migration Steps

  1. Initialize the structured project:
npx -y @agent8/gameserver-node init
  1. Convert your code:

    • Move code from server.js to server/src/server.ts
    • Add export to your Server class
    • Add TypeScript type annotations
  2. File Organization:

    • If your server.js is large (>200 lines), consider splitting into multiple files
    • You can organize code in several ways:

    Option A: Keep everything in Server class (no client changes needed)

    server/src/ server.ts (main server class with all functions) utils/ calculations.ts (helper functions - not exposed to clients)
    • Client code remains the same: remoteFunction('hello')

    Option B: Use handler namespaces with @Handler decorator (requires client updates)

    server/src/ server.ts (main server class) gameHandler.ts (@Handler('game') for game logic) userHandler.ts (@Handler('user') for user management) utils/ calculations.ts (helper functions)
    • Important: Using handlers requires updating client code
    • Example: remoteFunction('hello') becomes remoteFunction('game.hello')
    • Choose this only if you want to organize functions into namespaces

Migration Example

Before (server.js):

class Server { async updateScore(points) { const myState = await $global.getMyState(); const newScore = (myState.score || 0) + points; await $global.updateMyState({ score: newScore }); return newScore; } }

After (server/src/server.ts):

export class Server { async updateScore(points: number): Promise<number> { const myState = await $global.getMyState(); const newScore = (myState.score || 0) + points; await $global.updateMyState({ score: newScore }); return newScore; } }

Key Changes:

  • Add export keyword
  • Add type annotations for parameters (points: number)
  • Add return type (Promise<number>)
  • For large files, you can split into multiple files or use utility modules
⚠️

About @Handler Decorator: Using @Handler for namespaces is optional. If you use it:

  • Client code must be updated: remoteFunction('hello')remoteFunction('game.hello')
  • Choose this only if you want organized namespaces
  • If you want to avoid client changes, keep all functions in the main Server class
  1. Write Tests:

    • Create tests in server/test/server.test.ts
    • Test all migrated functions
    • Run npx -y @agent8/gameserver-node test to verify
  2. Deploy:

    • Once tests pass, push to repository
    • The platform will automatically build and deploy

After successful migration and deployment, you can delete the old server.js file from the project root.

Last updated on