Message-Driven Full Stack Development: How Odin Reduced Friction Between Angular and .NET
- Javi Herranz
- 6 days ago
- 8 min read

Modern full stack development is often shaped by a familiar pattern: a frontend application communicates with a backend API, the backend exposes endpoints, both sides agree on request and response contracts, and developers maintain the glue that keeps everything aligned.
This is a proven approach, and it remains the right choice in many systems. But at Odin we recently faced a situation where the usual model felt heavier than it needed to be. We were building corporate intranet applications where delivery speed, clarity and developer productivity mattered more than creating a broad public API surface.
The technical solution we adopted was a message-driven full stack framework for Angular and .NET. The idea was simple, but powerful in practice: instead of asking developers to create controllers, routes, DTOs, TypeScript interfaces and Angular services for every new feature, we made the backend message handler the single source of truth. From that handler, the platform generates the frontend contracts and exposes the operation automatically.
This was not an academic exercise, and it was not an attempt to prove that REST or traditional APIs are wrong. We had a real delivery problem. Too much engineering time was being spent on plumbing. Some of that plumbing was useful, but some of it felt like paying a tax every time we added or changed a small feature.

Rethinking the API Boundary
The initial motivation came from a practical delivery scenario. We were looking at several corporate intranet web applications that needed to be delivered in parallel, with a limited number of full stack developers. The applications were not extremely large, but they still required a complete stack: Angular on the frontend, .NET and C# on the backend, and SQL Server as the main data platform.
During estimation, it became obvious that the business logic itself was not the only cost. A lot of effort was going into maintaining the boundary between frontend and backend. In previous projects, we had seen entire chunks of development time disappear into synchronising C# DTOs, TypeScript interfaces, Angular services and backend endpoints. It was not particularly difficult work, but it was repetitive, easy to get wrong, and frankly not the best use of developer time.
In a traditional approach, adding a feature normally involves several steps. A developer creates a backend endpoint, defines request and response DTOs, updates or creates an Angular service, creates TypeScript interfaces, and then keeps all of those pieces in sync as the feature evolves. For corporate intranet applications, that overhead can become disproportionate. The architecture may be perfectly valid, but the workflow is slower than it needs to be.
This led us to a more direct question: what if HTTP was treated only as a transport mechanism, and the actual application contract was a strongly typed message?
A Message-Driven Model
The solution we adopted is based on a simple idea: the backend acts as a message processor.
Each operation is represented by a message. That message can be a command, when the intent is to change something, or a query, when the intent is to retrieve data. Each message has one handler, and that handler declares both the input contract and the output contract.
This creates a clear and predictable model: the message defines what is sent, the handler defines what happens, the response type defines what comes back, and the generated frontend code exposes the operation to Angular. In practice, the handler becomes the boundary of the feature.

This is inspired by CQRS principles, but applied in a pragmatic way. Technically, commands and queries are handled by the same infrastructure, but the distinction remains useful because it helps developers organise features by intent. A query should be read-oriented. A command should represent an action or change.
From Backend Handler to Angular Service
The most important part of the platform is the generated code pipeline.
When the backend is built, the platform scans the compiled message assembly.
Handlers are discovered through attributes, and the framework extracts the request and response types. From that information, it creates an intermediate manifest, similar in spirit to a service contract document.
A separate generator then consumes this manifest and produces TypeScript code for the Angular application. The generated output includes request classes, response interfaces and a single generated message service. This means that a developer can add a new backend handler, build the API, and then use that operation from Angular without manually creating frontend contracts.
A simplified backend handler looks like this:

From the Angular side, the generated code can then be consumed through the generated service:

The important detail here is that both sides of the call are strongly typed. The request is not an anonymous object shaped manually in Angular; it is a generated TypeScript class with the same fields as the C# command. The response is also generated as a TypeScript interface, so result exposes its properties directly in the editor with full IntelliSense support.
This also means that changes in the backend contract are propagated quickly to the frontend. If a field is renamed or removed from the request or response type in C#, the generated TypeScript code changes accordingly. Any Angular code still using the old property will fail at compile time, forcing the issue to be fixed immediately and keeping both sides aligned instead of allowing frontend and backend contracts to drift apart.
There is no need to manually create an Angular service for this feature. There is no need to manually duplicate the request and response types. The generated request class already contains the metadata required to call the backend message endpoint.
This is where the developer experience improves noticeably. The developer does not need to remember where a specific REST controller is defined, which Angular service wraps it, or whether the TypeScript interface still matches the C# response. The message handler is the contract.
A Thin Runtime Layer
At runtime, all message calls go through a generic endpoint:

The message type is included in the URL and used to resolve the correct handler.
Internally, the framework maintains a cached lookup between message types and handlers. The current implementation uses reflection for discovery and routing, which introduces a small runtime cost, but this is acceptable for the type of corporate intranet applications the framework was designed for.
In the future, this could be optimised further by generating the handler registry at build time, removing the need for reflection during runtime dispatch. For now, the current balance is a practical one: simple enough to maintain, fast enough for the workload, and transparent enough for developers to understand.
This URL structure also provides a practical debugging benefit. When inspecting network calls in the browser developer tools, the message being sent to the server is visible directly in the request URL. That makes it much easier to trace a frontend action back to its backend handler. A developer can inspect the browser console, identify the message name, and quickly locate the corresponding handler in Visual Studio or any other backend development environment.

This may sound like a small detail, but in day-to-day development it matters a lot. Anyone who has spent time hunting through frontend wrappers, controller actions and DTO folders knows how quickly that friction adds up. Faster navigation means faster debugging, and faster debugging means more time spent solving actual business problems instead of searching through plumbing code.
Supporting Real Application Needs
Although the model is intentionally simple, it still needs to support real application scenarios.
Most message calls use JSON serialization, which is the default. However, some features require file uploads, such as importing a spreadsheet or submitting a form with an attached document. For this reason, the framework also supports form-based serialization.
From the frontend, the developer can choose the serialization mode when sending the message:

The base message service handles the creation of the form payload, including files and other fields. On the backend, the handler does not need to care whether the original request was sent as JSON or as form data. It receives the message as a normal request object.
The same principle applies to error handling. Because all messages pass through the same endpoint, error handling can be centralised. Serialization errors, validation errors, database errors and general application errors can be translated into a consistent response structure.
Authorization is also handled through metadata on the message type. Instead of scattering authorization checks across multiple controllers, the request type can declare the roles required to execute the message. The infrastructure reads those attributes at runtime and applies the appropriate checks before the handler is executed.

Improving Productivity, Not Just Reducing Code
The main benefit of this approach is not simply that it generates code. Code generation is only useful when it removes repetitive work without hiding the important parts of the system.
In this case, the benefit is that developers can work at the feature level. They can create a handler, define the request and response close to the logic, build the project, and immediately consume the operation from Angular.
This improves productivity in several ways. It reduces duplication, because the same contract no longer needs to be manually represented in both C# and TypeScript. It reduces drift, because frontend contracts are generated from backend handlers. It improves discoverability, because features are organised as vertical slices and the handler is the main unit to search for, review and document. And it reduces context switching, because developers spend less time moving between controllers, DTO folders, service files and frontend wrappers.
This also makes the system easier to explain to new developers. The basic rule is clear: decide whether the feature is a command or a query, create the handler in the correct slice, and let the platform generate the frontend contract.
For us, this was the real win. Not just fewer files. Not just fewer lines of code. A smoother path from “we need this feature” to “the feature is available in the UI”.
Trade-offs and When This Approach Fits
This architecture is deliberately opinionated. It is not intended as a universal replacement for REST APIs or public API design.
There are trade-offs, and we accepted them deliberately. The frontend and backend are more tightly coupled. Message names are part of the route by default, although they can be customised through attributes when a clearer or versioned URL is needed. Each message can only have one handler, which simplifies the model but limits more advanced routing scenarios. The runtime handler resolution currently uses reflection, which is acceptable for corporate intranet applications but may need optimisation at larger scale.
For external APIs, partner integrations or systems with independent frontend and backend release cycles, a more traditional API contract may still be the better choice.
But for applications developed by the same team, deployed in a controlled intranet environment and built with productivity as a major goal, the coupling was not a flaw in the design. It was part of the design. We were not trying to create a platform for every possible integration scenario. We were trying to make a specific class of full stack applications faster and cleaner to build.
Final Thoughts
At Odin, this approach reflects a broader engineering principle: architecture should serve the delivery context.
In this case, the delivery context required speed, clarity and a strong developer experience. By treating backend operations as messages and generating the Angular contracts automatically, we reduced one of the most common sources of friction in full stack development.
The result is not just less boilerplate. It is a more direct way of building features, where the backend handler becomes the contract, the frontend remains strongly typed, and developers can move from idea to implementation with fewer manual steps.
For the type of corporate intranet applications this platform was designed for, that simplicity is a significant advantage.





