1. 20Introducing boundaries into an existing UI
  2. 21Relocating an independent feature

Migrating an existing UI

20

Introducing boundaries into an existing UI

Create a frontend-facing boundary inside the code that already ships before moving an existing mixed UI anywhere else.

By Eyal Ellenbogen September 11, 2026 3 min read

Starting from scratch is wonderful. No legacy baggage, no broken tests, and all the architectural boundaries you could ever dream of.

But reality doesn't work that way. In an established product, you inherit screens that grew one reasonable change at a time: a single file that fetches data, maps payloads, checks permissions, and renders markup because that was the fastest path to production three years ago. You will never get a hall pass to pause the roadmap for a rewrite, so the boundary has to be carved into code that is already in flight.

Before moving files into a cleaner folder structure, draw the boundary in place.

Draw the boundary in place

We can treat our candidate feature as a unit of change while leaving its files where they sit. Rather than asking what the backend API provides, we should write down what the interface actually requires in frontend-facing terms: loading data, saving a record, and checking an allowed action. That minimal contract is the feature's port.

Why not move the files now? Because moving code to a new directory changes the file tree, not the architecture. If a view is still coupled to global state and live endpoints, tucking it into a features folder only creates the illusion of modularity.

Invert the dependency

We can define the port right beside the existing view. Instead of importing backend clients directly, the view can point at a function contract it owns:

// Before: view calls backend client directly
const user = await authApi.getCurrentUser();

// After: view calls a function signature it owns
const user = await loadUser();

Beside the screen, an adapter function can fulfill loadUser by calling the existing client. In the browser, nothing looks or behaves differently. The route, network requests, and visual behavior stay identical. But the dependency direction is now inverted: the view depends on a function signature it defines rather than an external client it happens to import.

From there, we can unify multiple endpoints into a single UI-facing contract. A legacy view may juggle several network clients at once—fetching user data from one endpoint, looking up permissions from another, and stitching them together in component state.

We can collapse that coordination into a single function type:

// The view depends on a single function contract
type LoadProfileFn = () => Promise<UserProfile>;

// The adapter function coordinates endpoints and maps the result
const loadProfileHttp: LoadProfileFn = async () => {
  const user = await userApi.getCurrentUser();
  const permissions = await permissionsApi.getPermissions(user.id);

  return {
    name: user.name,
    canEdit: permissions.includes('admin'),
  };
};

The view now makes one call: await loadProfile().

The coordination logic did not disappear; it moved to the adapter function. If the backend later combines those endpoints or alters permission shapes, the adapter absorbs the change. The view never knows or cares how many HTTP requests were required.

That initial boundary should stay lean. The port should reflect what this screen needs now, rather than every endpoint the backend happens to offer.

Do the edge before the folder

With an explicit contract in place, file reorganization finally delivers on its promise. A feature folder, a dedicated package, or an isolated workspace exists to house code that can already stand on its own.

Order of operations

First
Port in place, keep shipping
Then
Move the independent code to its home

Verify the new boundary

Check the view's imports before moving anything. The boundary is holding if:

  • The view imports only its port and visual primitives.
  • An API schema change only requires editing the adapter.
  • A visual redesign never mentions network code.

Once those hold, we can move the feature into its own home.