Your front end is building against a copy of your API
In one of my more recent engineering lead roles, most of the features we shipped needed a UI and a backend at the same time. Almost every one of them started the same argument.
The front-end engineers wanted the backend finished before they began. They'd been burned before by building screens against an API that changed underneath them, and waiting for something real felt safer.
The backend engineers felt that as pressure to hit a deadline that wasn't theirs. And once they'd given the front end a version of the API, they felt stuck with it. A renamed field or a restructured payload, or a status they only discovered halfway through implementation, turned into a negotiation. So they either kept a design they now knew was wrong, or changed it and wrecked someone else's week.
QA were caught in the middle. They tested the backend on its own, then tested the front end on its own against whatever it had been built on, then tested the two together, mostly by hand, because the integrated build was where the surprises turned up. That was the same feature verified three times, and only the last pass told us whether it worked.
One case has stuck with me. The backend engineers had modelled a status field as a plain string, because early on nobody knew which states that entity would end up having, and a string was the quickest thing that worked. Once the states became clear, they refactored it into an enum with a fixed set of values. That was the right thing to do, and I'd have asked for it anyway. Downstream, though, it hurt. The front end had built screens and logic around a free-text field, and QA had test cases written against it. Both now had to deal with a change to the data type itself, plus a new set of behaviours and validations that came with the fixed states.
I don't think anyone on those teams was bad at their job. Given how the work was set up, each of them was behaving sensibly.
The contract was treated as a handoff
Put the three complaints side by side. The front end won't start until the API stops moving. The backend can't move the API once the front end has started. QA has to check by hand whether the two still line up.
They're one problem: the API between the two teams kept changing, and nobody could see it change.
We had two ways to deal with that, and I disliked both. We could work in sequence, backend first and front end after, which is safe and slow. Or we could agree the API up front and freeze it, which lets both teams work in parallel and stops the backend from acting on anything it learns during implementation. We tried each at different times. Working in sequence cost us velocity, which hurts more than usual on a team that's supposed to be agile. Freezing gave us brittle releases and plenty of sticking points whenever the two halves were integrated.
I wanted a third option. Agree the API early, let both sides build in parallel, let the backend change its mind, and hear about every change on the day it happens instead of the day QA opens the integrated build.
The front end never really waited
It took me longer than I'd like to admit to see this. Even when we made the front end wait, they didn't. The moment they started, they built against something: a folder of JSON fixtures, a mock server, a few hard-coded responses in a service layer. That something was a mock of the backend's API, written from the agreed contract or from an early response somebody pasted into chat.
A mock is a copy of an API taken at one moment. On the day it's written it's accurate. After that the backend keeps changing and the mock doesn't. People call this mock drift, and on a full-stack feature it starts the first time a backend engineer improves their own design.
The front end's tests pass, because the front end agrees with its mock. The backend's tests pass, because the backend agrees with itself. The drift sits in the space between them, and the only thing checking that space was a person in QA clicking through the integrated build.
Why the spec didn't help
At one point we adopted a version of contract-first development. Before anyone wrote code, the teams agreed an OpenAPI spec for the feature, and the front end built its mocks from that spec. On paper it solved the waiting problem.
In practice, the spec was written before anyone had built the thing it described. Sooner or later the backend would find that some detail planned on paper didn't survive implementation: a field that needed a different shape, a state nobody had anticipated, a validation that belonged somewhere else. Each time, there were two uncomfortable choices. The backend could bend the implementation to fit a spec they now knew was wrong, or amend the spec and hope everyone who had built on the old version noticed. The spec turned into something to negotiate over, which is the same friction we started with, moved into a YAML file.
Amending it didn't fix the mocks either. When the backend updated the spec and the code together, any check that the backend matched the spec passed, because both had moved. The front end's mocks were still built from the version they'd been given weeks earlier, so they described an API that no longer existed.
Checking the API against the spec and checking it against the front end's mock are separate questions, and they fail for different reasons. When the spec moves with the code, the first one almost never fails. The second one is what would have caught the status refactor.
What changes when someone checks that gap
Suppose the mock gets compared against the running backend automatically, every night and on every backend merge.
The front end can start on the first day. The agreed contract lives as mock files in the repository, and they build against those from the first morning. They no longer need the API to stay still. They need to hear when it moves, and now they will.
The backend can change its mind again. A change to the API stops being a broken promise and becomes a normal event: it gets deployed to the dev environment, the check goes red within hours, and the output says what moved, whether that's a renamed field, a string that is now a number, or a new property. The conversation happens that day, with the details in front of everyone, while the change is still cheap.
QA stop checking the seam by hand. Whether the two halves still agree on the shape of the data has an automated answer, so their integrated testing can focus on behaviour, which is the part that needs a person's judgement.
Mock updates get reviewed. When a change is expected, the mock is updated from the live backend and the update lands as a pull request. The front-end engineer reviews it like any other change to something their code depends on.
Habits that matter more than the tool
Run the check against a deployed backend as well as on front-end commits. Drift arrives on the backend's schedule, so a check that only runs when the front end pushes is looking at the wrong weeks.
Compare shapes and ignore values. Different data in a response isn't drift. A field that changed type, appeared, or disappeared is. If the check compares values it will produce so much noise that someone switches it off within a month.
Go after the error responses and edge states on purpose. People write mocks from the happy path because that's what they see first. Drift tends to hide in the states you exercise least: empty lists, validation errors, records in an unusual state. Those are the screens that break at integration.
Make "couldn't reach the backend" fail differently from a clean run. Dev environments go down. If an unreachable backend counts as a pass, the check turns green on exactly the day it matters and nobody notices.
Don't let mocks update themselves without review. Regenerating them from live responses automatically just moves the surprise from integration to production. Someone on the consuming side should read every change.
Where APIFae comes in
I've built versions of this more than once, as scripts that never survived the next reorganisation. Eventually I wrote it properly as APIFae, a single binary with no runtime to install. apifae diff checks every mocked endpoint against a live URL and reports drift from the mock separately from violations of the spec:
$ apifae diff "$DEV_BACKEND_URL"
Comparing against https://dev.example.internal
GET /items/{id}
✗ Drift detected:
~ $.status: String → Number
Summary: 1 checked, 1 drifted
It exits 0 when clean, 1 on drift, and 2 when it couldn't finish the run, so a dev environment that's down never passes for a clean result. apifae patch then updates the mock files and leaves the change in git diff for someone to review. The full loop is in Keep a mock honest.
With that running against our dev backend, the status refactor would have shown up as a red check on the day it merged. The front end and QA would have heard about it while it was still a small change to talk through, instead of finding it afterwards.
The API between your front end and your backend is going to change, because the people building it learn things as they go. I'd stop trying to freeze it and start checking it instead.