Echoes in Orionjs provide a structured way to implement event-driven architecture and handle inter-service communication. Using the @Echoes()
and @EchoRequest()
or @EchoEvent()
decorators, you can easily create handlers for both synchronous requests and asynchronous events.
Creating Echo Controllers
An echoes controller is a class decorated with @Echoes()
that contains methods decorated with @EchoRequest()
or @EchoEvent()
:
import { EchoRequest, EchoEvent, Echoes, createEchoRequest, createEchoEvent } from '@orion-js/echoes'
import { Inject } from '@orion-js/services'
import { ExampleRepository } from '../repos/ExampleRepository'
import { schemaWithName, InferSchemaType } from '@orion-js/schema'
// Define schema for parameters and return type
export const ExampleSchema = schemaWithName('ExampleSchema', {
_id: { type: String },
name: { type: String },
createdAt: { type: Date }
})
// Infer TypeScript type from schema
export type ExampleType = InferSchemaType<typeof ExampleSchema>
@Echoes()
export class GetDataEchoes {
@Inject(() => ExampleRepository)
private exampleRepository: ExampleRepository
@EchoRequest()
getDataById = createEchoRequest({
params: {
exampleId: { type: String }
},
returns: ExampleSchema,
resolve: async (params) => {
return await this.exampleRepository.getExampleById(params.exampleId)
}
})
}
Echo Request Handlers
Use the @EchoRequest()
decorator with the createEchoRequest()
function to define methods that handle synchronous requests from other services:
@EchoRequest()
getUserById = createEchoRequest({
params: {
userId: { type: String }
},
returns: UserSchema,
resolve: async (params) => {
return await this.userRepository.findById(params.userId)
}
})
Echo Event Handlers
Use the @EchoEvent()
decorator with the createEchoEvent()
function to define methods that process asynchronous events:
@Echoes()
export class UserEventsEchoes {
@Inject(() => EmailService)
private emailService: EmailService
@EchoEvent()
userRegistered = createEchoEvent({
params: {
user: { type: UserSchema }
},
resolve: async (params) => {
await this.emailService.sendWelcomeEmail(params.user.email)
}
})
}
Event Decorator Options
The createEchoEvent()
function accepts options similar to createEchoRequest()
:
@EchoEvent()
processSomething = createEchoEvent({
attemptsBeforeDeadLetter: 5,
params: {
// schema definition
},
resolve: async (params) => {
// implementation
}
})
Making Requests
To make a request to another service:
import { request } from '@orion-js/echoes'
// In a service or resolver
async function getUserDetails(userId: string): Promise<UserDetails> {
return await request({
service: 'users', // Target service name
method: 'getUserById', // Method name in the target service
params: { userId }, // Parameters to pass
timeout: 5000, // Optional timeout in milliseconds
retries: 3 // Optional number of retries
})
}
Publishing Events
To publish an event for other services to consume:
import { publish } from '@orion-js/echoes'
// In a service after creating a user
async function createUser(userData: UserInput): Promise<User> {
const user = await this.userRepository.create(userData)
// Publish an event
await publish({
topic: 'userRegistered', // Event topic
params: { user }, // Event payload
acks: 1, // Optional: number of acknowledgments
timeout: 3000 // Optional: timeout in milliseconds
})
return user
}
Starting the Echoes Service
To enable echoes in your application, you need to configure and start the echoes service:
import { startService } from '@orion-js/echoes'
import { app } from '@orion-js/http'
// Start the echoes service
await startService({
// Kafka client configuration (for events)
client: {
clientId: 'my-app',
brokers: ['kafka:9092']
},
// Request configuration (for synchronous communication)
requests: {
key: 'shared-secret-key', // Secret key for request signing
handlerPath: '/echoes-services', // Path for HTTP handlers
services: {
users: 'http://users-service:3000',
payments: 'http://payments-service:3000'
}
},
// Advanced options
readTopicsFromBeginning: true, // Read missed messages when reconnecting
partitionsConsumedConcurrently: 4 // Number of partitions to consume concurrently
})
Error Handling
Echoes automatically handles errors in request and event handlers:
@EchoRequest()
processPayment = createEchoRequest({
params: { type: PaymentParamsSchema },
returns: PaymentResultSchema,
resolve: async (params) => {
try {
const result = await this.paymentService.processPayment(params)
return result
} catch (error) {
// Errors are automatically propagated back to the requester
// with proper error classification (UserError, ValidationError, etc.)
throw new Error(`Payment processing failed: ${error.message}`)
}
}
})
Custom Error Types
Orionjs handles special error types appropriately:
- UserError: For expected application errors
- ValidationError: For data validation errors
import { UserError } from '@orion-js/helpers'
import { ValidationError } from '@orion-js/schema'
@EchoRequest()
validateUser = createEchoRequest({
params: {
userId: { type: String }
},
resolve: async (params) => {
const user = await this.userRepository.findById(params.userId)
if (!user) {
throw new UserError('USER_NOT_FOUND', 'User was not found')
}
if (!user.isActive) {
throw new ValidationError({
status: 'User account is inactive'
})
}
}
})
Those errors will be automatically propagated back to the requester.
Type Safety
Using TypeScript with schema inference, you can ensure type safety for your echo handlers:
// Define schemas for strong typing
const CreateOrderParamsSchema = schemaWithName('CreateOrderParams', {
customerId: { type: String },
items: {
type: [{
productId: { type: String },
quantity: { type: Number }
}]
}
})
const OrderResultSchema = schemaWithName('OrderResult', {
orderId: { type: String },
total: { type: Number },
status: { type: String, allowedValues: ['pending', 'completed'] }
})
// Infer types from schemas
type CreateOrderParamsType = InferSchemaType<typeof CreateOrderParamsSchema>
type OrderResultType = InferSchemaType<typeof OrderResultSchema>
@EchoRequest()
createOrder = createEchoRequest({
params: CreateOrderParamsSchema,
returns: OrderResultSchema,
resolve: async (params: CreateOrderParamsType): Promise<OrderResultType> => {
// Implementation with full type safety
}
})
Best Practices
-
Organize by Domain: Group related echo handlers in the same controller class.
-
Leverage Dependency Injection: Use @Inject(() => Service)
to access repositories and services.
-
Keep Methods Focused: Each echo handler should have a clear, single responsibility.
-
Use Strong Typing: Define parameter and return types with schemas and infer TypeScript types.
-
Handle Errors Gracefully: Catch and properly categorize errors.
-
Idempotent Handlers: Design event handlers to be idempotent (safe to process the same event multiple times).
-
Timeout Configuration: Set appropriate timeouts for requests based on expected execution time.
-
Security: Use the shared key to secure inter-service communication.
-
Service Discovery: Keep the service registry updated when adding new services.
-
Monitoring: Implement proper logging and monitoring for echo handlers.