August 11, 2026
Migrating a REST service safely: reusing E2E tests to verify external side effects
The problem with rewriting a live API
Migrating a REST service to a new framework — Flask to FastAPI, an old Express app to something else — is rarely risky because of the code you're rewriting. It's risky because of everything downstream that depends on the behavior of that code: webhooks fired on state changes, records written to a reconciliation store, callbacks sent to a payment processor, events published to a queue. Unit tests on the new implementation tell you the new code does what you think it does. They don't tell you it does what the old code did, byte for byte, for every consumer that isn't in your repository.
Write the E2E suite against the old service first
Before touching the migration, write end-to-end tests against the existing service, treating it as a black box: real HTTP requests in, real responses and side effects out. Cover:
- Every endpoint's happy path and its documented error responses.
- Idempotency behavior — does calling it twice with the same payload produce the same result?
- The side effects that matter operationally, not just the HTTP response: does it write to the database, fire a webhook, publish a message?
This suite is now your specification. It's more trustworthy than any written doc, because it's the actual observed contract of the running system.
Reuse the same suite against the migration
Point the exact same test suite at the new implementation, changing only the base URL (or running both side by side against a shared test environment). Any failure is a real behavioral difference, not a guess. This turns "does the migration work?" from a judgment call into a pass/fail gate, and it's the same suite you'll keep running in CI afterward — no throwaway test code.
The part people skip: verifying external side effects
Passing your own E2E suite proves your service still behaves the same from the outside. It doesn't prove the systems downstream of you got the same effects. If your service reconciles transactions into an external ledger, sends payroll confirmations, or triggers a webhook another team's service consumes, that's where regressions hide — because they don't show up in your service's response, only in what your service caused elsewhere.
Two ways to close that gap with the same test investment:
- Assert on the external system's state, not just your response. If the test suite is already calling your API and expecting a webhook or a downstream write, extend the assertion to poll or query the external system (a mock server standing in for the payment processor, a queue consumer, a test double for the third-party API) and confirm it received the same payload shape and count as under the old implementation.
- Record and diff, when you can't assert directly. For third-party integrations you don't control, capture the outbound requests during both the old-service and new-service runs (a recording proxy works well here) and diff them. Identical outbound payloads and headers are strong evidence the external contract hasn't shifted, even if you can't inspect the third party's internal state.
Why this pays for itself
The upfront cost is writing the E2E suite before you're allowed to touch the migration. The payoff is that the same suite becomes permanent regression coverage, the acceptance gate for the cutover, and — with the external-effects extension — the thing that catches the failure mode that actually causes incidents: not "the new code returns the wrong JSON," but "the new code silently stopped notifying the system that processes the money."
More posts
What actually happens between tapping a card and the terminal saying "Approved"
The two-to-three second round trip of a card payment, traced hop by hop: terminal, acquirer, card network, issuer, and back — plus why authorization and settlement are two completely different processes.
Branching strategy: main, pre, and dev
A practical branching model for teams that need a real pre-production gate: main, pre, and short-lived feature branches, with naming conventions and a two-step PR flow.
Running old and new in parallel: coexistence during a partial migration
A migration is rarely a single cutover. Here's how to let a partially migrated system and the legacy one it's replacing serve traffic at the same time, safely, until the cutover is actually done.