As the chatbot matures, another requirement arrives: support more than one LLM provider.
This is a common inflection point. Early on, it is convenient to wire the backend directly to a single SDK. The response from the provider flows straight through the server and into the UI. It feels efficient.
It also quietly couples your entire stack to a vendor’s response shape.
If that vendor changes its format, or if you decide to introduce a second provider, the ripple effects can reach the frontend quickly. What began as a backend integration detail becomes a full-stack coordination problem.
In the chatbot’s evolution, this risk is handled differently. Instead of exposing raw provider responses, the system defines a provider interface that returns a domain-level ChatReply. Each provider implementation adapts its own SDK response into that shared shape.
The rest of the application does not know or care which provider generated the reply. It only understands the domain contract.
This decision seems architectural, but it has very practical consequences. Switching providers or introducing a second one no longer forces a redesign of shared types. The volatility is contained. The surface area of change is smaller.
TypeScript reinforces this separation. The compiler ensures that every provider implementation produces a valid ChatReply. The backend handler depends on the interface, not on vendor-specific JSON.
In a growing system, this is what stability looks like. The parts that are likely to change are isolated behind clear contracts. The parts that need to remain steady, especially the frontend, are shielded from that churn.