WebSocket Plugins
Build type-safe WebSocket URLs and validate messages with WebSocket plugins.
Overview
WebSocket plugins extend your WebSocket contracts with runtime utilities for URL construction and message validation. Unlike HTTP plugins which use .use(), WebSocket plugins use .useWebSocket().
Installation
WebSocket plugins are included in @ts-contract/plugins:
pnpm add @ts-contract/pluginswebsocketPathPlugin
The websocketPathPlugin adds a buildPath() method to each WebSocket definition for type-safe URL construction.
Basic Usage
import { createContract, initContract } from '@ts-contract/core';
import { websocketPathPlugin } from '@ts-contract/plugins';
import { z } from 'zod';
const contract = createContract({
chat: {
type: 'websocket',
path: '/ws/chat/:roomId',
pathParams: z.object({ roomId: z.string() }),
query: z.object({ token: z.string().optional() }),
clientMessages: {},
serverMessages: {},
},
});
const api = initContract(contract)
.useWebSocket(websocketPathPlugin)
.build();
// Build WebSocket URL
const url = api.chat.buildPath({ roomId: '123' });
// => "/ws/chat/123"
// With query parameters
const urlWithQuery = api.chat.buildPath(
{ roomId: '123' },
{ token: 'abc-token' }
);
// => "/ws/chat/123?token=abc-token"Type Safety
TypeScript enforces correct parameter types:
// ✓ Valid
api.chat.buildPath({ roomId: '123' });
// ✗ Error: Type 'number' is not assignable to type 'string'
api.chat.buildPath({ roomId: 123 });
// ✗ Error: Property 'roomId' is missing
api.chat.buildPath({});URL Encoding
Path parameters and query values are automatically URL-encoded:
const url = api.chat.buildPath(
{ roomId: 'room with spaces' },
{ token: 'special@chars' }
);
// => "/ws/chat/room%20with%20spaces?token=special%40chars"Common Use Cases
Client-side WebSocket connections:
const url = api.chat.buildPath({ roomId });
const ws = new WebSocket(`wss://api.example.com${url}`);Phoenix.js integration:
import { Socket } from 'phoenix';
const path = api.chat.buildPath({ roomId: '123' });
const socket = new Socket('/socket');
const channel = socket.channel(`chat:${roomId}`);websocketValidatePlugin
The websocketValidatePlugin adds validation methods for WebSocket messages and connection parameters.
Basic Usage
import { createContract, initContract } from '@ts-contract/core';
import { websocketValidatePlugin } from '@ts-contract/plugins';
import { z } from 'zod';
const contract = createContract({
chat: {
type: 'websocket',
path: '/ws/chat/:roomId',
pathParams: z.object({ roomId: z.string() }),
clientMessages: {
new_msg: z.object({
type: z.literal('new_msg'),
body: z.string().min(1),
}),
},
serverMessages: {
new_msg: z.object({
type: z.literal('new_msg'),
id: z.string(),
body: z.string(),
userId: z.string(),
}),
},
},
});
const api = initContract(contract)
.useWebSocket(websocketValidatePlugin)
.build();Validation Methods
validateClientMessage()
Validates outgoing client messages:
validateClientMessage<EventName>(eventName: EventName, data: unknown)Example:
// ✓ Valid
const msg = api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
body: 'Hello!',
});
// ✗ Throws validation error - missing body
api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
});
// ✗ Throws validation error - empty body
api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
body: '',
});validateServerMessage()
Validates incoming server messages:
validateServerMessage<EventName>(eventName: EventName, data: unknown)Example:
// ✓ Valid
const msg = api.chat.validateServerMessage('new_msg', {
type: 'new_msg',
id: '123',
body: 'Hello from server',
userId: 'user-456',
});
// ✗ Throws validation error - missing userId
api.chat.validateServerMessage('new_msg', {
type: 'new_msg',
id: '123',
body: 'Hello',
});validatePathParams()
Validates WebSocket connection path parameters:
const params = api.chat.validatePathParams({ roomId: '123' });
// => { roomId: '123' }validateQuery()
Validates WebSocket connection query parameters:
const query = api.chat.validateQuery({ token: 'abc' });
// => { token: 'abc' }validateHeaders()
Validates WebSocket connection headers:
const contract = createContract({
chat: {
type: 'websocket',
path: '/ws/chat',
headers: {
authorization: z.string(),
},
clientMessages: {},
serverMessages: {},
},
});
const api = initContract(contract)
.useWebSocket(websocketValidatePlugin)
.build();
const headers = api.chat.validateHeaders({
authorization: 'Bearer token',
});Using Both Plugins Together
Most applications use both plugins:
import { websocketPathPlugin, websocketValidatePlugin } from '@ts-contract/plugins';
const api = initContract(contract)
.useWebSocket(websocketPathPlugin)
.useWebSocket(websocketValidatePlugin)
.build();
// Build URL
const url = api.chat.buildPath({ roomId: '123' });
// Validate outgoing message
const msg = api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
body: 'Hello!',
});
// Validate incoming message
channel.on('new_msg', (data) => {
const validated = api.chat.validateServerMessage('new_msg', data);
console.log(validated.body);
});Client-Side Integration
Phoenix.js Example
import { Socket } from 'phoenix';
const socket = new Socket('/socket', {
params: { token: userToken },
});
socket.connect();
const roomId = '123';
const topic = `chat:${roomId}`;
const channel = socket.channel(topic, {});
// Validate and handle incoming messages
channel.on('new_msg', (data: unknown) => {
try {
const msg = api.chat.validateServerMessage('new_msg', data);
displayMessage(msg);
} catch (error) {
console.error('Invalid message:', error);
}
});
// Send validated messages
function sendMessage(body: string) {
const msg = api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
body,
});
channel.push('new_msg', msg)
.receive('ok', () => console.log('Sent'))
.receive('error', (err) => console.error('Failed:', err));
}
channel.join();Native WebSocket Example
const url = api.chat.buildPath({ roomId: '123' });
const ws = new WebSocket(`wss://api.example.com${url}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
try {
const msg = api.chat.validateServerMessage('new_msg', data);
console.log('Received:', msg.body);
} catch (error) {
console.error('Invalid message:', error);
}
};
function sendMessage(body: string) {
const msg = api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
body,
});
ws.send(JSON.stringify(msg));
}Error Handling
Validation errors include descriptive messages:
try {
api.chat.validateClientMessage('new_msg', {
type: 'new_msg',
body: '', // Empty string
});
} catch (error) {
console.error(error.message);
// => "Validation failed for client message 'new_msg' of /ws/chat/:roomId: String must contain at least 1 character(s)"
}Type Safety
All validation methods return properly typed results:
// TypeScript knows the exact message type
const msg = api.chat.validateClientMessage('new_msg', data);
// msg is typed as: { type: 'new_msg', body: string }
const serverMsg = api.chat.validateServerMessage('new_msg', data);
// serverMsg is typed as: { type: 'new_msg', id: string, body: string, userId: string }Performance Considerations
- websocketPathPlugin: Minimal overhead (~500 bytes minified)
- websocketValidatePlugin: Depends on schema library (Zod, Valibot, etc.)
Validation is recommended at system boundaries:
- ✅ Validate incoming messages from server
- ✅ Validate outgoing messages before sending
- ❌ Skip validation for internal message processing
Next Steps
- See WebSocket Contracts for contract definition
- Explore Phoenix.js Chat Recipe for complete example
- Learn about Creating Custom Plugins
- Review Plugin System for how plugins work