Event Definition Guide
Learn how to define type-safe event schemas for your WebSocket communication.
Basic Event Definition
Events are defined using defineEvents() with Zod schemas:
import { defineEvents } from '@mdrv/wsx/shared'
import { z } from 'zod'
const events = defineEvents({
// Event name
ping: {
request: z.object({ timestamp: z.number() }), // Request schema
response: z.object({ pong: z.string() }), // Response schema
},
})
Event Types
1. Request/Response Events
Events that expect a response from the server:
const events = defineEvents({
getData: {
request: z.object({ id: z.string() }),
response: z.object({
name: z.string(),
age: z.number()
}),
},
})
// Client usage
const data = await client.request('getData', { id: '123' })
console.log(data.name, data.age) // TypeScript knows the types!
// Server usage
server.onRequest('getData', async (payload) => {
return { name: 'Alice', age: 30 } // Must match response schema
})
2. One-Way Messages (Client → Server)
Events without a response - fire and forget:
const events = defineEvents({
notify: {
request: z.object({ message: z.string() }),
// No response field
},
})
// Client usage
client.send('notify', { message: 'Hello!' })
// Server usage
server.onSend('notify', async (payload) => {
console.log(payload.message)
// No return value
})
3. Server Push Events (Server → Client)
Events initiated by the server:
const events = defineEvents({
serverUpdate: {
request: z.object({ data: z.any() }),
},
})
// Server pushes to client
server.onRequest('subscribe', async (payload, connection) => {
setInterval(() => {
connection.send('serverUpdate', {
data: { timestamp: Date.now() }
})
}, 1000)
return { subscribed: true }
})
// Client listens
client.on('serverUpdate', (payload) => {
console.log('Update:', payload.data)
})
Schema Patterns
Optional Fields
const events = defineEvents({
updateUser: {
request: z.object({
id: z.string(),
name: z.string().optional(),
age: z.number().optional(),
}),
response: z.object({ updated: z.boolean() }),
},
})
Nested Objects
const events = defineEvents({
createPost: {
request: z.object({
title: z.string(),
content: z.string(),
author: z.object({
id: z.string(),
name: z.string(),
}),
tags: z.array(z.string()),
}),
response: z.object({
postId: z.string(),
}),
},
})
Enums and Literals
const events = defineEvents({
setStatus: {
request: z.object({
status: z.enum(['online', 'offline', 'away']),
visibility: z.literal('public').or(z.literal('private')),
}),
response: z.object({ success: z.boolean() }),
},
})
Unions and Discriminated Unions
const events = defineEvents({
processData: {
request: z.discriminatedUnion('type', [
z.object({ type: z.literal('text'), content: z.string() }),
z.object({ type: z.literal('number'), value: z.number() }),
z.object({ type: z.literal('image'), url: z.string().url() }),
]),
response: z.object({ processed: z.boolean() }),
},
})
Validation Rules
const events = defineEvents({
createUser: {
request: z.object({
username: z.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be at most 20 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Only alphanumeric and underscore allowed'),
email: z.string().email('Invalid email address'),
age: z.number().int().positive().max(150),
password: z.string().min(8),
}),
response: z.object({
userId: z.string().uuid(),
}),
},
})
Type Extraction
Extract TypeScript types from your event definitions:
import type { InferEventPayload, InferEventResponse } from '@mdrv/wsx/shared'
const events = defineEvents({
getData: {
request: z.object({ id: z.string() }),
response: z.object({ name: z.string(), age: z.number() }),
},
})
// Extract request type
type GetDataRequest = InferEventPayload<typeof events, 'getData'>
// => { id: string }
// Extract response type
type GetDataResponse = InferEventResponse<typeof events, 'getData'>
// => { name: string, age: number }
// Use in functions
function processData(data: GetDataRequest): GetDataResponse {
return { name: 'Alice', age: 30 }
}
Sharing Event Definitions
Create a shared file that both client and server import:
// shared/events.ts
import { defineEvents } from '@mdrv/wsx/shared'
import { z } from 'zod'
export const events = defineEvents({
ping: {
request: z.object({ timestamp: z.number() }),
response: z.object({ pong: z.string() }),
},
notify: {
request: z.object({ message: z.string() }),
},
})
// server.ts
import { events } from './shared/events'
const { server, handler } = createElysiaWS(events)
// client.ts
import { events } from './shared/events'
const client = createClient(url, events)
Best Practices
1. Use Descriptive Event Names
// ✅ Good
const events = defineEvents({
getUserProfile: { ... },
updateUserSettings: { ... },
deletePost: { ... },
})
// ❌ Bad
const events = defineEvents({
get: { ... },
update: { ... },
delete: { ... },
})
2. Keep Schemas Simple
// ✅ Good - simple, focused
const events = defineEvents({
createUser: {
request: z.object({
name: z.string(),
email: z.string().email(),
}),
response: z.object({ id: z.string() }),
},
})
// ❌ Avoid - too complex
const events = defineEvents({
createUser: {
request: z.object({
data: z.object({
personal: z.object({
names: z.object({
first: z.string(),
middle: z.string().optional(),
last: z.string(),
}),
}),
}),
}),
},
})
3. Add Validation Messages
const events = defineEvents({
login: {
request: z.object({
email: z.string()
.email('Please provide a valid email'),
password: z.string()
.min(8, 'Password must be at least 8 characters'),
}),
response: z.object({
token: z.string(),
}),
},
})
4. Version Your Events
When making breaking changes, create new event names:
const events = defineEvents({
// Old version (deprecated but keep for compatibility)
getUserV1: {
request: z.object({ id: z.string() }),
response: z.object({ name: z.string() }),
},
// New version with more fields
getUserV2: {
request: z.object({ id: z.string() }),
response: z.object({
name: z.string(),
email: z.string(),
age: z.number(),
}),
},
})
See Also
- Shared API Reference -
defineEvents()API - Validation Guide - Runtime validation
- Client API - Using events with client
- Server API - Handling events on server