---
title: "Server Side"
description: "Build game servers with @esengine/server"
---

## Quick Start

Create a new game server project using the CLI:

```bash
# Using npm
npm create esengine-server my-game-server

# Using pnpm
pnpm create esengine-server my-game-server

# Using yarn
yarn create esengine-server my-game-server
```

Generated project structure:

```
my-game-server/
├── src/
│   ├── shared/              # Shared protocol (client & server)
│   │   ├── protocol.ts      # Type definitions
│   │   └── index.ts
│   ├── server/              # Server code
│   │   ├── main.ts          # Entry point
│   │   └── rooms/
│   │       └── GameRoom.ts  # Game room
│   └── client/              # Client example
│       └── index.ts
├── package.json
└── tsconfig.json
```

Start the server:

```bash
# Development mode (hot reload)
npm run dev

# Production mode
npm run start
```

## createServer

Create a game server instance:

```typescript
import { createServer } from '@esengine/server'
import { GameRoom } from './rooms/GameRoom.js'

const server = await createServer({
    port: 3000,
    onConnect(conn) {
        console.log('Client connected:', conn.id)
    },
    onDisconnect(conn) {
        console.log('Client disconnected:', conn.id)
    },
})

// Register room type
server.define('game', GameRoom)

// Start server
await server.start()
```

### Configuration Options

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `port` | `number` | `3000` | WebSocket port |
| `tickRate` | `number` | `20` | Global tick rate (Hz) |
| `apiDir` | `string` | `'src/api'` | API handlers directory |
| `msgDir` | `string` | `'src/msg'` | Message handlers directory |
| `httpDir` | `string` | `'src/http'` | HTTP routes directory |
| `httpPrefix` | `string` | `'/api'` | HTTP routes prefix |
| `cors` | `boolean \| CorsOptions` | - | CORS configuration |
| `onStart` | `(port) => void` | - | Start callback |
| `onConnect` | `(conn) => void` | - | Connection callback |
| `onDisconnect` | `(conn) => void` | - | Disconnect callback |

## HTTP Routing

Supports HTTP API sharing the same port with WebSocket, ideal for login, registration, and similar scenarios.

```typescript
const server = await createServer({
    port: 3000,
    httpDir: './src/http',   // HTTP routes directory
    httpPrefix: '/api',       // Route prefix
    cors: true,

    // Or inline definition
    http: {
        '/health': (req, res) => res.json({ status: 'ok' })
    }
})
```

> For detailed documentation, see [HTTP Routing](/en/modules/network/http)

## Room System

Room is the base class for game rooms, managing players and game state.

### Define a Room

```typescript
import { Room, Player, onMessage } from '@esengine/server'
import type { MsgMove, MsgChat } from '../../shared/index.js'

interface PlayerData {
    name: string
    x: number
    y: number
}

export class GameRoom extends Room<{ players: any[] }, PlayerData> {
    // Configuration
    maxPlayers = 8
    tickRate = 20
    autoDispose = true

    // Room state
    state = {
        players: [],
    }

    // Lifecycle
    onCreate() {
        console.log(`Room ${this.id} created`)
    }

    onJoin(player: Player<PlayerData>) {
        player.data.name = 'Player_' + player.id.slice(-4)
        player.data.x = Math.random() * 800
        player.data.y = Math.random() * 600

        this.broadcast('Joined', {
            playerId: player.id,
            playerName: player.data.name,
        })
    }

    onLeave(player: Player<PlayerData>) {
        this.broadcast('Left', { playerId: player.id })
    }

    onTick(dt: number) {
        // State synchronization
        this.broadcast('Sync', { players: this.state.players })
    }

    onDispose() {
        console.log(`Room ${this.id} disposed`)
    }

    // Message handlers
    @onMessage('Move')
    handleMove(data: MsgMove, player: Player<PlayerData>) {
        player.data.x = data.x
        player.data.y = data.y
    }

    @onMessage('Chat')
    handleChat(data: MsgChat, player: Player<PlayerData>) {
        this.broadcast('Chat', {
            from: player.data.name,
            text: data.text,
        })
    }
}
```

### Room Configuration

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `maxPlayers` | `number` | `10` | Maximum players |
| `tickRate` | `number` | `20` | Tick rate (Hz) |
| `autoDispose` | `boolean` | `true` | Auto-dispose empty rooms |

### Room API

```typescript
class Room<TState, TPlayerData> {
    readonly id: string           // Room ID
    readonly players: Player[]    // All players
    readonly playerCount: number  // Player count
    readonly isLocked: boolean    // Lock status
    state: TState                 // Room state

    // Broadcast to all players
    broadcast<T>(type: string, data: T): void

    // Broadcast to all except one
    broadcastExcept<T>(type: string, data: T, except: Player): void

    // Get player by ID
    getPlayer(id: string): Player | undefined

    // Kick a player
    kick(player: Player, reason?: string): void

    // Lock/unlock room
    lock(): void
    unlock(): void

    // Dispose room
    dispose(): void
}
```

### Lifecycle Methods

| Method | Trigger | Purpose |
|--------|---------|---------|
| `onCreate()` | Room created | Initialize game state |
| `onJoin(player)` | Player joins | Welcome message, assign position |
| `onLeave(player)` | Player leaves | Cleanup player data |
| `onTick(dt)` | Every frame | Game logic, state sync |
| `onDispose()` | Before disposal | Save data, cleanup resources |

## Player Class

Player represents a connected player in a room.

```typescript
class Player<TData = Record<string, unknown>> {
    readonly id: string          // Player ID
    readonly roomId: string      // Room ID
    data: TData                  // Custom data

    // Send message to this player
    send<T>(type: string, data: T): void

    // Leave room
    leave(): void
}
```

## @onMessage Decorator

Use decorators to simplify message handling:

```typescript
import { Room, Player, onMessage } from '@esengine/server'

class GameRoom extends Room {
    @onMessage('Move')
    handleMove(data: { x: number; y: number }, player: Player) {
        // Handle movement
    }

    @onMessage('Attack')
    handleAttack(data: { targetId: string }, player: Player) {
        // Handle attack
    }
}
```

## Schema Validation

Use the built-in Schema validation system for runtime type validation:

### Basic Usage

```typescript
import { s, defineApiWithSchema } from '@esengine/server'

// Define schema
const MoveSchema = s.object({
    x: s.number(),
    y: s.number(),
    speed: s.number().optional()
})

// Auto type inference
type Move = s.infer<typeof MoveSchema>  // { x: number; y: number; speed?: number }

// Use schema to define API (auto validation)
export default defineApiWithSchema(MoveSchema, {
    handler(req, ctx) {
        // req is validated, type-safe
        console.log(req.x, req.y)
    }
})
```

### Validator Types

| Type | Example | Description |
|------|---------|-------------|
| `s.string()` | `s.string().min(1).max(50)` | String with length constraints |
| `s.number()` | `s.number().min(0).int()` | Number with range and integer constraints |
| `s.boolean()` | `s.boolean()` | Boolean |
| `s.literal()` | `s.literal('admin')` | Literal type |
| `s.object()` | `s.object({ name: s.string() })` | Object |
| `s.array()` | `s.array(s.number())` | Array |
| `s.enum()` | `s.enum(['a', 'b'] as const)` | Enum |
| `s.union()` | `s.union([s.string(), s.number()])` | Union type |
| `s.record()` | `s.record(s.any())` | Record type |

### Modifiers

```typescript
// Optional field
s.string().optional()

// Default value
s.number().default(0)

// Nullable
s.string().nullable()

// String validation
s.string().min(1).max(100).email().url().regex(/^[a-z]+$/)

// Number validation
s.number().min(0).max(100).int().positive()

// Array validation
s.array(s.string()).min(1).max(10).nonempty()

// Object validation
s.object({ ... }).strict()  // No extra fields allowed
s.object({ ... }).partial() // All fields optional
s.object({ ... }).pick('name', 'age')  // Pick fields
s.object({ ... }).omit('password')     // Omit fields
```

### Message Validation

```typescript
import { s, defineMsgWithSchema } from '@esengine/server'

const InputSchema = s.object({
    keys: s.array(s.string()),
    timestamp: s.number()
})

export default defineMsgWithSchema(InputSchema, {
    handler(msg, ctx) {
        // msg is validated
        console.log(msg.keys, msg.timestamp)
    }
})
```

### Manual Validation

```typescript
import { s, parse, safeParse, createGuard } from '@esengine/server'

const UserSchema = s.object({
    name: s.string(),
    age: s.number().int().min(0)
})

// Throws on error
const user = parse(UserSchema, data)

// Returns result object
const result = safeParse(UserSchema, data)
if (result.success) {
    console.log(result.data)
} else {
    console.error(result.error)
}

// Type guard
const isUser = createGuard(UserSchema)
if (isUser(data)) {
    // data is User type
}
```

## Protocol Definition

Define shared types in `src/shared/protocol.ts`:

```typescript
// API request/response
export interface JoinRoomReq {
    roomType: string
    playerName: string
}

export interface JoinRoomRes {
    roomId: string
    playerId: string
}

// Game messages
export interface MsgMove {
    x: number
    y: number
}

export interface MsgChat {
    text: string
}

// Server broadcasts
export interface BroadcastSync {
    players: PlayerState[]
}

export interface PlayerState {
    id: string
    name: string
    x: number
    y: number
}
```

## Client Connection

```typescript
import { connect } from '@esengine/rpc/client'

const client = await connect('ws://localhost:3000')

// Join room
const { roomId, playerId } = await client.call('JoinRoom', {
    roomType: 'game',
    playerName: 'Alice',
})

// Listen for broadcasts
client.onMessage('Sync', (data) => {
    console.log('State:', data.players)
})

client.onMessage('Joined', (data) => {
    console.log('Player joined:', data.playerName)
})

// Send message
client.send('RoomMessage', {
    type: 'Move',
    payload: { x: 100, y: 200 },
})
```

## ECSRoom

`ECSRoom` is a room base class with ECS World support, suitable for games that need ECS architecture.

### Server Startup

```typescript
import { Core } from '@esengine/ecs-framework';
import { createServer } from '@esengine/server';
import { GameRoom } from './rooms/GameRoom.js';

// Initialize Core
Core.create();

// Global game loop
setInterval(() => Core.update(1/60), 16);

// Create server
const server = await createServer({ port: 3000 });
server.define('game', GameRoom);
await server.start();
```

### Define ECSRoom

```typescript
import { ECSRoom, Player } from '@esengine/server/ecs';
import { Component, ECSComponent, sync } from '@esengine/ecs-framework';

// Define sync component
@ECSComponent('Player')
class PlayerComponent extends Component {
    @sync("string") name: string = "";
    @sync("uint16") score: number = 0;
    @sync("float32") x: number = 0;
    @sync("float32") y: number = 0;
}

// Define room
class GameRoom extends ECSRoom {
    onCreate() {
        this.addSystem(new MovementSystem());
    }

    onJoin(player: Player) {
        const entity = this.createPlayerEntity(player.id);
        const comp = entity.addComponent(new PlayerComponent());
        comp.name = player.id;
    }
}
```

### ECSRoom API

```typescript
abstract class ECSRoom<TState, TPlayerData> extends Room<TState, TPlayerData> {
    protected readonly world: World;     // ECS World
    protected readonly scene: Scene;     // Main scene

    // Scene management
    protected addSystem(system: EntitySystem): void;
    protected createEntity(name?: string): Entity;
    protected createPlayerEntity(playerId: string, name?: string): Entity;
    protected getPlayerEntity(playerId: string): Entity | undefined;
    protected destroyPlayerEntity(playerId: string): void;

    // State sync
    protected sendFullState(player: Player): void;
    protected broadcastSpawn(entity: Entity, prefabType?: string): void;
    protected broadcastDelta(): void;
}
```

### @sync Decorator

Mark component fields that need network synchronization:

| Type | Description | Bytes |
|------|-------------|-------|
| `"boolean"` | Boolean | 1 |
| `"int8"` / `"uint8"` | 8-bit integer | 1 |
| `"int16"` / `"uint16"` | 16-bit integer | 2 |
| `"int32"` / `"uint32"` | 32-bit integer | 4 |
| `"float32"` | 32-bit float | 4 |
| `"float64"` | 64-bit float | 8 |
| `"string"` | String | Variable |

## Best Practices

1. **Set Appropriate Tick Rate**
   - Turn-based games: 5-10 Hz
   - Casual games: 10-20 Hz
   - Action games: 20-60 Hz

2. **Use Shared Protocol**
   - Define all types in `shared/` directory
   - Import from here in both client and server

3. **State Validation**
   - Server should validate all client inputs
   - Never trust client-sent data

4. **Disconnect Handling**
   - Implement reconnection logic
   - Use `onLeave` to save player state

5. **Room Lifecycle**
   - Use `autoDispose` to clean up empty rooms
   - Save important data in `onDispose`
