Real-time applications are everywhere. From live support chats to stock tickers, users expect instant updates. While WebSockets are often the go-to solution, Server-Sent Events (SSE) provide a simpler, lighter alternative for unidirectional (server-to-client) data flow.
In this tutorial, we'll build a multi-channel live chat API using InversifyJS for dependency injection and Hono as our high-performance web framework.
💡 Get the Code: The complete source code for this example is available in the InversifyJS Framework Examples repository.
Why this stack?
- InversifyJS: Provides powerful Dependency Injection (DI) to keep your services and controllers decoupled and testable.
- Hono: A small, fast, and web-standards based web framework that runs anywhere (Node.js, Bun, Deno, Cloudflare Workers).
- SSE: Native browser support, automatic reconnection, and no need for heavy WebSocket protocols for simple broadcast scenarios.
1. Project Setup
First, let's install the necessary dependencies. We need inversify for DI, hono for the server, and the Inversify adapters.
npm install hono inversify reflect-metadata @inversifyjs/http-hono @inversifyjs/http-sse @hono/node-server
Note: Make sure you have experimentalDecorators and emitDecoratorMetadata enabled in your tsconfig.json.
2. Define the Typed Message
We start by defining what our chat message looks like. Using TypeScript interfaces ensures type safety throughout our app.
// src/types/ChatMessage.ts
export interface ChatMessage {
content: string;
sender: string;
timestamp?: string;
}
3. The Channel Service
The heart of our real-time logic. This service manages active SSE connections (streams) for each chat channel.
// src/services/ChannelService.ts
import { injectable } from 'inversify';
import { SseStream } from '@inversifyjs/http-sse';
import { ChatMessage } from '../types/ChatMessage.js';
@injectable()
export class ChannelService {
// Store active streams per channel
private readonly channels: Map<string, Set<SseStream>> = new Map();
public subscribe(channelId: string): SseStream {
if (!this.channels.has(channelId)) {
this.channels.set(channelId, new Set());
}
const streams = this.channels.get(channelId);
if (!streams) {
throw new Error(`Channel ${channelId} not found`);
}
const stream = new SseStream();
streams.add(stream);
// Cleanup on disconnect
stream.on('close', () => {
streams.delete(stream);
if (streams.size === 0) {
this.channels.delete(channelId);
}
});
return stream;
}
public async publish(channelId: string, message: ChatMessage): Promise<void> {
const streams = this.channels.get(channelId);
if (!streams) return;
// Broadcast message to all connected clients in the channel
const promises = Array.from(streams).map(async (stream) =>
stream.writeMessageEvent({
data: JSON.stringify(message),
type: 'message',
}),
);
await Promise.all(promises);
}
}
4. The Controller
We use Inversify's @Controller to define our API endpoints.
-
GET /channels/:channelId/messages: Subscribes a client to the channel using@SsePublisher. -
POST /channels/:channelId/messages: Publishes a new message.
// src/controllers/ChannelController.ts
import { Controller, Get, Post, Body, Params } from '@inversifyjs/http-core';
import {
SsePublisher,
SsePublisherOptions,
SseStream,
} from '@inversifyjs/http-sse';
import { inject } from 'inversify';
import { ChannelService } from '../services/ChannelService.js';
import { ChatMessage } from '../types/ChatMessage.js';
@Controller('/channels')
export class ChannelController {
constructor(
@inject(ChannelService) private readonly channelService: ChannelService,
) {}
@Get('/:channelId/messages')
public subscribe(
@Params('channelId') channelId: string,
@SsePublisher() publisher: (options: SsePublisherOptions) => unknown,
): unknown {
const stream: SseStream = this.channelService.subscribe(channelId);
// Return the stream wrapped in the publisher
return publisher({ events: stream });
}
@Post('/:channelId/messages')
public async publish(
@Params('channelId') channelId: string,
@Body() body: ChatMessage,
): Promise<void> {
await this.channelService.publish(channelId, body);
}
}
5. Wiring It All Together
Finally, we set up the Inversify container and the Hono adapter.
// src/index.ts
import 'reflect-metadata';
import { Container } from 'inversify';
import { InversifyHonoHttpAdapter } from '@inversifyjs/http-hono';
import { serve } from '@hono/node-server';
import { ChannelService } from './services/ChannelService.js';
import { ChannelController } from './controllers/ChannelController.js';
async function main() {
const container = new Container();
// Bind dependencies
container.bind(ChannelService).toSelf().inSingletonScope();
container.bind(ChannelController).toSelf();
// Create the Hono App with Inversify
const adapter = new InversifyHonoHttpAdapter(container);
const app = await adapter.build();
console.log('Server started on http://localhost:3000');
serve({
fetch: app.fetch,
port: 3000,
});
}
await main();
Conclusion
With just a few files, we've created a scalable, type-safe architecture for real-time communication. The combination of Hono's speed and InversifyJS's structure makes it easy to build and maintain complex applications.
Check out the full InversifyJS framework Documentation for more examples!

Top comments (0)