DEV Community

Cover image for FE/BE - Unite Them!
Typing Turtle
Typing Turtle

Posted on

FE/BE - Unite Them!

tl;dr;

  • Teams should agree upon and understand the source of truth domain, working together and evolving it together
  • Unification is a conceptual project; having more to do with organizational and team operational structure than code/architecture

You may notice that the ideas here are nothing new and are well represented in DDD (Domain Driven Design). For more into this topic I highly recommend reading Learning Domain Driven Design


We're rewriting a large system at Anedot. We're using rails as a backend to develop a public API which will be consumed by a bleeding edge TanStack app on the frontend, but is built to be consumed by anyone.

Classically teams are split by frontend and backend specialization. This can lead to "model drift" as conceptual models of the domain that exist within the rails system can become a deeply different beast from the frontend UI models and user flows. This conceptual understanding of the model will eventually lead to differences in code and types.

Over time this "drift" means the frontend lacks understanding of the system capabilities and the backend loses sight of the purpose of it all - features delivered to the user.

Over time, FE ends up:

  • thinking in API responses, not domain concepts
  • optimizing for screens, not system behavior
  • missing invariants and edge cases enforced in the backend
  • discovering issues only after integration

That’s not a tooling issue, it’s an information flow problem.

In most teams, this is exactly what happens: the frontend becomes a consumer of shapes rather than a participant in the system’s meaning. Over time, that leads to shallow understanding and poorer decisions.

The solution is structured exposure to domain decisions and constraints.

The Source of Truth

If we can all agree on the source of truth for the domain, we can focus on a shared understanding and vigilant maintenance of that model.

  • So is it the shared public API (OpenAPI spec)?
  • Or the frontend algebraic data types?
  • Or the database schema? (we considered generating typescript types from the rails schema)

The reality is that these will always diverge unless you constrain the system.

The domain model should sit in the center. DB, API, and UI are projections of that model.

        UI (view models)
             ↑
        API contract (OpenAPI spec)
             ↑
        Domain model  ← this is the real source of truth
             ↓
        Persistence (DB schema)
Enter fullscreen mode Exit fullscreen mode

If you skip explicitly defining the domain model, the API becomes an accidental one, and drift is inevitable.

The pitfalls of other options

The API is the contract for integration boundaries. APIs are often consumer-optimized, not domain-accurate. They hide or reshape domain concepts and so using them as a source of truth would result in leaky abstractions.

Tightly coupling the DB schema directly in FE leads to tight coupling to storage decisions.

Overall we need both interface correctness and conceptual integrity. We need a good integration boundary and shared understanding of the domain.

How this works

1. Define an explicit domain layer.

  • Named domain objects (not DB tables, not API DTOs)
  • Clear invariants
  • Shared terminology

Constraints on the domain model implemented in rails should be reflected in the TS types.

Example:

// domain (conceptual)
type Subscription = {
  id: string
  status: "active" | "past_due" | "canceled"
  plan: Plan
  billingCycle: BillingCycle
}
Enter fullscreen mode Exit fullscreen mode

2. Treat API schemas as a projection of the domain

The OpenAPI spec should be derived from (or at least aligned with) domain concepts.

Bad:

{
  "user_id": "...",
  "plan_id": "...",
  "stripe_subscription_id": "..."
}
Enter fullscreen mode Exit fullscreen mode

Better:

{
  "subscriptionId": "...",
  "status": "active",
  "plan": { ... }
}
Enter fullscreen mode Exit fullscreen mode

3. Accept that FE models ≠ API models

Trying to unify FE + API types directly is a trap.

Example:

type SubscriptionCard = {
  title: string
  isActive: boolean
  renewalDateFormatted: string
}
Enter fullscreen mode Exit fullscreen mode

4. Prevent drift with process, not just tooling

No "Throw over the wall".

Cross-functional ownership. Teams aligned with features, not function. Same management, same backlog, same project, same retrospectives and plannings/shapings, same post-mortems.

In a word, shared ownership and cultivation of the source of truth domain.

Before building:

  • Define domain concept
  • Define API shape
  • Validate against real user flows

Every API change should answer:

  • What domain concept does this represent?
  • Is this shaped for a real workflow?
  • Are we leaking DB structure?

Unite Them!

Given our constraint for a public API we can generate projections in the correct direction:

DB -> Rails (domain) -> OpenAPI spec -> typescript types -> UI types -> UI
Enter fullscreen mode Exit fullscreen mode

Overall it's important to think about both the technology and human side of software to arrive at truly useful systems while maintaining reliability and speed of development.

Top comments (0)