One of the principles I learned years ago is that the frontend should be able to run without the backend. No database. No event store. No handlers. Just the UI, talking to endpoints that return hardcoded responses. That’s an approach I’ve taken for granted since then.

That constraint sounds artificial until you see how much it frees the team to move.

The Old Way

In one project, we did it with hardcoded Angular services. The component would call a service method, and instead of making an HTTP call, the service would just return a static object with the data we needed. We’d feed those objects with whatever values made the screen look plausible, and the developer could build the UI component as if the backend already existed.

It worked, and it gave us the discipline of thinking about data shapes before implementation. But it had a downside: those fake services lived in the codebase alongside real ones, and over time you’d find ghosts of old approaches that nobody had cleaned up.

The Better Way: Stub the Endpoint

A few years into the project, we evolved the approach. Instead of faking things on the frontend, we create real endpoints on the backend that return stubbed data.

What that means in practice: the controller exists, the action exists, the authorization check is in place, and when someone calls the endpoint, it returns a hardcoded JSON response that matches the agreed-upon contract. It takes very little time to set up. No database table needs to exist. No handler needs to be wired. No aggregate, no projection, no event sourcing infrastructure at all.

And because it’s a real endpoint returning real JSON, the frontend developer can write code exactly as they would for the finished feature. The Angular service makes an actual HTTP call. The component handles an actual response. The only thing to remove later is the stubbed response, swapping the stubbed implementation for a real one.

What It Looks Like in Practice

Here is a stubbed GET endpoint for a “new recipe” route — the kind used to initialize a form before the user creates a record:

[HttpGet("new")]
[Authorize(Policy = PolicyDefinitions.Purchasing.CanCreateOrder)]
public async Task New()
{
    return Ok(this.GetJsonFromEmbeddedResource("new_stubbed_response"));
}

The GetJsonFromEmbeddedResource call loads a hardcoded JSON file compiled into the assembly. No database. No handler. The response is exactly what the contract says it should be.

For an edit route, the pattern is the same but takes an id from the route:

[HttpGet("{id}/edit")]
[Authorize(Policy = PolicyDefinitions.Purchasing.CanUpdateOrder)]
public ActionResult Edit([FromRoute] Guid id)
{
    return Ok(this.GetJsonFromEmbeddedResource("edit_stubbed_response"));
}

And for a POST — a create or submit action — the controller accepts a dynamic payload as a placeholder until the real command handler is wired:

[HttpPost]
[Authorize(Policy = PolicyDefinitions.Purchasing.CanCreateOrder)]
public async Task Create([FromBody] dynamic payload)
{
    return Ok(this.GetJsonFromEmbeddedResource("create_stubbed_response"));
}

In every case, the authorization check is real. The route is real. The response shape is real. The only thing that is stubbed is the implementation.

What the Tests Verify

We also write a test for each stubbed endpoint, and it checks two things: that the endpoint is protected by the correct permission (calling it without the right permission returns a 403 forbidden), and that the response matches the shape of the contract we designed.

This is important. The test isn’t verifying business logic — there is none yet. It’s verifying the contract. If the endpoint returns a field the contract doesn’t include, or is missing a field the contract requires, the test fails. That means the frontend developer knows immediately what to expect, and the backend developer knows if they’ve drifted.

Here is what the test class looks like. Two facts, two scenarios:

public class happy_path : IntegrationTestBase, IClassFixture>
{
    public happy_path(WebApplicationFactoryBase factory, ITestOutputHelper output)
        : base(factory, output)
    {
        ValidPermission = "Purchasing.Order.Create";
        SetEndpointBuilder(() => "api/v1/purchasing/orders/new");
    }

    [Fact]
    public void authorized()
    {
        given_user_is_authorized();
        when_GETting();
        then_response_matches("expected_response");
    }

    [Fact]
    public void unauthorized()
    {
        given_user_is_NOT_authorized();
        when_GETting();
        then_response_is_forbidden();
    }
}

The then_response_matches assertion compares the response body against a JSON file checked into the test project. The contract is explicit and version-controlled.

When the real implementation lands later, these same tests continue to pass. They verify the same contract with live data instead of hardcoded data.

The decision not to use existing tools that offer mock APIs for testing was deliberate; the team’s process structure took us very close to what the ultimate endpoint designs would be, so the home-grown harness (including test support) enabled us to eliminate waste by putting us on track to move forward constantly.

Parallel Development Without Coordination Overhead

The real benefit shows up at the team level. Once the endpoint design is agreed on and the stubs are in place, the frontend and backend can be implemented independently. There’s no need to check “is the endpoint ready yet?” or “can you at least return something so I can test my component?” The endpoint is already there. It already returns the right shape. Work proceeds in parallel.

In our team, this was how we got so much done in a sprint with two developers. One person would take the frontend, the other the backend, and we’d meet at integration time. Most of the time, it just worked.

This process is yielding dividends as we orchestrate AI agents that also benefit from this way of working.

The Frontend Is as Dumb as Possible

There’s a design philosophy behind all of this: the frontend should be as dumb as possible. All validation rules, all business logic, all data shaping happens on the backend. The frontend renders what the backend sends and passes back what the user provides.

When you enforce that, stub endpoints become the natural mechanism. The frontend never needs to fabricate its own data or make assumptions about what the backend will eventually provide. It trusts the contract, because the contract was designed before anything was built.

This principle extends to validation as well. Rather than duplicating validation logic on the frontend, the backend exposes a “recipe” — a description of the rules the frontend should apply. I wrote about this approach in a four-part series: Part 1, Part 2, Part 3, and Part 4. The recipe travels with the stub endpoint from the start, so the frontend form is wired to real validation rules before the full business logic implementation exists on the backend.

What I learned is that the discipline of “run the UI without the backend” isn’t about the demo or the prototype. It’s about forcing clarity on the contract early, when changing it is still cheap.

Leave a Reply

Trending

Discover more from Claudio Lassala's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading