Intro – Inspired by a PR Comment
This post was triggered by a PR comment: ‘Isn’t this wrapper overkill for MVP?’ I’ve been there before — and I’ve learned that a little early structure saves a lot of late pain.
Why I Stick to Facades and Wrappers From the Start
Even when moving quickly, I find it worth using:
- Per-feature facades** (e.g.
ChatFacade
,EmailFacade
) - Per-hub gateway services** (e.g.
ChatHubGateway
,EmailHubGateway
) - DTO mappers
They:
- Add barely any overhead at the start.
- Keep backend transport concerns (SignalR/HTTP) isolated.
- Make future changes predictable — especially when multiple hubs are involved.
- Save me from pulling tangled logic out of components later.
Even if the app doesn’t go far, it’s a minimal investment for peace of mind.
What I Include Early (Post-MVP Stability)
- Per-Hub Gateway Services
Each hub (chat
, email
, agent
) gets its own wrapper for connection and event handling.
@Injectable({ providedIn: 'root' })
export class ChatHubGateway {
#conn = new HubConnectionBuilder().withUrl('/chatHub').build();
#msg$ = new Subject<ChatMessage>();
message$ = this.msg$.asObservable();
start() {
this.conn.on('ReceiveMessage', m => this.msg$.next(m));
return this.conn.start();
}
}
2. Typed DTO Mapping
I always keep backend shapes away from UI models.
toChatMessage(dto: ChatMessageDto): ChatMessage {
return { sender: dto.from, text: dto.content, timestamp: new Date(dto.ts) };
}
3. Connection Status Signals
@Injectable({ providedIn: 'root' })
export class EmailHubService extends SignalRHubService {
readonly status = signal<'connected' | 'disconnected' | 'reconnecting'>('disconnected');
override startConnection() {
this.hubConnection.onreconnecting(() => this.status.set('reconnecting'));
this.hubConnection.onreconnected(() => this.status.set('connected'));
this.hubConnection.onclose(() => this.status.set('disconnected'));
return this.hubConnection.start();
}
}
Quick Note
Currently my understanding about signals is that they should only be used when data is required in the template, so the “status” above should only be used for informing the end user, I would ideally separate out observable and signal data into different services, I have been using naming convention like “emailHubDataService” for all observable things and then “emailHubService” for any signal based things.
In a component:
@Component({ ... })
export class EmailPanelComponent {
readonly connectionStatus = inject(EmailHubService).status;
}
Now you can show:
html
@if(isReconnecting)
<div>
Attempting to reconnect...
</div>
ts
const status = signal<'connected' | 'disconnected' | 'reconnecting'>('disconnected');
isReconnecting = computed(() => status() === 'reconnecting')
Tracking Incoming vs Outgoing Traffic
It helps to distinguish what’s being sent to the server vs what’s coming from the server. I’ve found it useful to separate these both semantically and in logging.
This is an area I have recently made some mistakes in and was informed that I had over engineered, (hence the inspiration for this post) I added a wrapper around the emailHubService and than added two new services to distinguish between incoming and outgoing calls, I understand now, that it was over engineered, my understanding just came from understanding the original hub services.
Semantic distinction:
An example of how what I wanted to achieve can be done without the separate services.
// OUTGOING (client → server)
sendDisconnect(...)
sendAcceptEmail(...)
senRejectEmail(...)
// INCOMING (server → client)
onReceiveEmailMessage(...)
registerHandlers() // binds handlers like 'ReceiveEmailMessage'
Logging:
Wrap both directions to log clearly:
// Outgoing
send<T>(method: string, payload: T) {
this.logger.debug(`[OUTGOING] ${method}`, payload);
this.hubConnection?.send(method, payload);
}
// Incoming
#handleIncoming<T>(label: string, payload: T) {
this.logger.debug(`[INCOMING] ${label}`, payload);
}
This makes tracing issues between frontend and backend a lot easier, especially when events stop flowing or are being sent with unexpected payloads.
Side Note:
The use of the “#” syntax in place of the “private” access modifier.
What I Leave Until Later
I think these should wait until there’s a clear need:
- Global state libraries (NgRx, Akita)
- Factories for creating hubs
- Generic event buses
- Central hub connection manager (unless coordinating 3+ hubs)
Observables First, Signals Later (Reminder to Self)
A quick personal rule:
Keep SignalR data as observables until it reaches the DOM — then convert to signals, if the template has a service, then I feel it is fine to convert it there too, just as long as it is not being reference around the rest of the codebase.
Why?
- Observables are better for streaming, retries, and cancellations.
- Signals are great for UI reactivity.
- This keeps the core data flow reactive without tying it to the DOM too early.
Typical usage:
@Component({ ... })
export class ChatComponent {
readonly messageSignal = toSignal(chatHub.message$);
}
Final Thought
This isn’t about gold-plating MVPs — it’s about laying groundwork that doesn’t cost much but saves me big later.
Even if nothing ships, I’d rather have clean wrappers and small abstractions than spend hours later undoing a spaghetti mess. If it all falls over? At least I didn’t build the mess twice.