A comprehensive TypeScript library for building robust Node.js services with Express, PostgreSQL, Redis, WebSockets, and more.
npm install @rhino-ai/orchard9-coretsx
- Configuration Management - Load and validate configuration from YAML files and environment variables
- Logging - Structured logging with child loggers and HTTP request tracking
- Database Operations - Knex-based database operations with transaction support
- Express Middleware - Common Express middleware for security, request parsing, and more
- Error Handling - Standardized error handling with custom error classes
- HTTP Responses - Standardized HTTP response formatting
- Validation - Request validation with Zod schemas
- Caching - Redis-based caching with memoization and batch loading
- WebSockets - WebSocket server with room management and authentication
- Event System - Pub/Sub event system for async communication
- Testing Utilities - Mocks for testing database, HTTP, and WebSocket operations
import { loadConfig, getConfigValue } from '@rhino-ai/orchard9-coretsx';
const appConfig = loadConfig({
files: ['./config/default.yaml', `./config/${process.env.NODE_ENV}.yaml`],
env: process.env
});
const httpLogs = getConfigValue(appConfig, 'http.logs');
import { logger, createChildLogger } from '@rhino-ai/orchard9-coretsx';
// Standard logging
logger.info({ userId, action: 'login' }, 'User logged in');
logger.error({ err, taskId }, 'Error performing task');
// Child loggers for per-request context
const log = createChildLogger({ requestId: req.id });
log.info('Handling user upload');
import { db, transaction } from '@rhino-ai/orchard9-coretsx';
// Simple query
const users = await db('users').where('active', true);
// In-transaction
await transaction(async (trx) => {
await trx('orders').insert({ /* … */ });
});
import {
queryParser,
bodySanitizer,
responseFormatter,
pagination,
validateRequest,
securityHeaders,
cors
} from '@rhino-ai/orchard9-coretsx';
app.use(queryParser());
app.use(bodySanitizer());
app.use(validateRequest({ /* schemas… */ }));
app.use(responseFormatter());
app.use(securityHeaders());
app.use(cors({ origin: ['https://example.com'] }));
import {
NotFoundError,
ValidationError,
createErrorHandler,
errorHandler,
logger
} from '@rhino-ai/orchard9-coretsx';
// Register routes first
app.use('/api/users', userRoutes);
app.use('/api/todos', todoRoutes);
// Then add error handling middleware
app.use((req, res, next) => next(new NotFoundError('Route not found')));
app.use(createErrorHandler({ logger }));
// Alternatively, when using createServer:
// 1. Set registerErrorHandler: false in createServer options
// 2. Register your routes
// 3. Manually add the error handler afterward
app.use(errorHandler);
import {
sendResponse,
sendErrorResponse,
sendPaginatedResponse
} from '@rhino-ai/orchard9-coretsx';
// Success response
sendResponse(res, {
data: todo,
message: 'Todo retrieved successfully'
});
// Error response
sendErrorResponse(res, 'Todo not found', 404);
// Paginated response
sendPaginatedResponse(res, todos, totalCount, { page, limit, offset });
import { validation, validateRequest } from '@rhino-ai/orchard9-coretsx';
import { z } from 'zod';
const todoSchema = z.object({
title: z.string().min(1).max(100),
description: z.string().max(500).optional(),
completed: z.boolean().default(false)
});
app.post('/todos',
validateRequest({ body: todoSchema }),
(req, res) => {
// Handle validated request
}
);
import { cache } from '@rhino-ai/orchard9-coretsx';
// Set a value
await cache.set('todo:123', { id: '123', title: 'Buy groceries' }, { ttl: 300 });
// Get a value
const todo = await cache.get('todo:123');
// Delete a value
await cache.del('todo:123');
import { websocket } from '@rhino-ai/orchard9-coretsx';
// Initialize
websocket.initialize(httpServer, { path: '/ws' });
// Handle connections
websocket.onConnection((socket, req, metadata) => {
console.log(`Client connected: ${metadata.clientId}`);
// Send welcome message
websocket.send(metadata.clientId, 'welcome', {
message: 'Welcome!'
});
});
// Handle messages
websocket.onMessage('todo:create', (message, socket, metadata) => {
// Process message
});
Messages sent to and from the WebSocket server follow a standard format:
interface WebSocketMessage<T = any> {
// Message type: 'system', 'user', 'error', 'notification', or 'control'
type: MessageType;
// Event name that identifies the action or topic
event: string;
// Message payload containing the data
payload: T;
// Optional unique message ID for correlation
id?: string;
// Optional timestamp (defaults to Date.now())
timestamp?: number;
// Optional message version
version?: string;
}
-
system
- System-level messages for connection management, heartbeats, and room operations -
user
- Regular application-specific messages (default if not specified) -
error
- Error notifications -
notification
- Notifications that don't require a response -
control
- Messages that control connection state or behavior
- For system messages:
ping
,room:join
,room:leave
,room:list
- For custom messages: Use descriptive names in the format
resource:action
(e.g.,user:update
,message:send
)
// Send a chat message
socket.send(JSON.stringify({
type: 'user',
event: 'chat:message',
payload: {
text: 'Hello world!',
roomId: 'room-123'
}
}));
The WebSocket server provides detailed error information when messages are invalid:
// Example error response for invalid JSON
{
"type": "error",
"event": "error",
"payload": {
"code": "invalid_json",
"message": "Failed to parse JSON",
"details": {
"error": "Unexpected token { in JSON at position 12",
"data": "{malformed{json"
}
},
"id": "err-123",
"timestamp": 1619712345678
}
Common error codes include:
-
invalid_data_type
- Message data is not a string, Buffer, or ArrayBuffer -
invalid_json
- Message contains malformed JSON -
missing_event
- Message is missing the required "event" property -
event_not_found
- No handlers registered for the specified event -
handler_error
- Error occurred while processing the message
import { events } from '@rhino-ai/orchard9-coretsx';
// Publish an event
await events.publish('user:created', {
id: 'user-123',
name: 'John Doe'
});
// Subscribe to an event
events.subscribe('user:created', async (payload) => {
console.log(`New user created: ${payload.name}`);
});
import {
createMockDbClient,
createMockHttpRequest,
createMockResponse
} from '@rhino-ai/orchard9-coretsx';
// Setup test mocks
const dbMock = createMockDbClient([{ id: 1, name: 'Alice' }]);
const req = createMockHttpRequest({ method: 'GET', url: '/users' });
const res = createMockResponse();
// Test your handler
await listUsers(req, res, () => {});
expect(res.jsonSpy).toHaveBeenCalledWith([{ id: 1, name: 'Alice' }]);
import {
createServer,
initializeDatabase,
errorHandler,
logger,
config
} from '@rhino-ai/orchard9-coretsx';
import routes from './api/routes';
async function bootstrap() {
try {
// Initialize database
initializeDatabase({
client: 'pg',
connection: {
host: config.get('db.host', 'localhost'),
port: config.get('db.port', 5432),
user: config.get('db.user', 'postgres'),
password: config.get('db.password', 'postgres'),
database: config.get('db.database', 'app')
}
});
// Create and start the server
const { app, server } = await createServer({
port: config.get('port', 8080),
host: config.get('host', '0.0.0.0'),
httpLogs: true,
autoStart: true,
registerErrorHandler: false, // Don't register error handler yet
health: {
path: '/health',
includeConnections: true
}
});
// Register API routes
app.use('/api', routes);
// Register error handler after all routes
app.use(errorHandler);
logger.info({ port: config.get('port', 8080) }, 'Service started');
return { app, server };
} catch (error) {
logger.error({ error }, 'Failed to start service');
process.exit(1);
}
}
bootstrap();
// 1. Built-ins (node, express, etc.)
import { Router } from 'express';
// 2. Third-party (e.g. lodash)
import { z } from 'zod';
// 3. Core library
import {
logger,
db,
sendResponse
} from '@rhino-ai/orchard9-coretsx';
// 4. Local modules
import { todoService } from './services';
- Classes / Types:
PascalCase
- Functions / Methods:
camelCase
- Variables:
camelCase
- Files:
kebab-case.ts
MIT