Modernising an App a Piece at a Time
My contract at Spidertracks involved extending an existing React and Redux application, migrating core JavaScript code to TypeScript, and building Node.js API endpoints to replace existing Java and Spring services.
It was work across two boundaries: the frontend's language and the backend's implementation. Neither change had to happen across the whole application at once. That is a useful shape for modernisation because it lets engineering effort stay connected to the product that already exists.
Start with the behavior
An existing application contains decisions that are easy to miss when you look only at its framework or language. A screen has loading behavior. A request has error behavior. A value that appears absent might be deliberately different from an empty value. Some of those decisions will be documented; others are visible only in the code and the way people use it.
When I approach this kind of work, I want the behavior in view before I improve its implementation. Otherwise a tidy replacement can change something important without anybody intending it.
For a React application, that means following an interaction through the component, the state it reads, the action it dispatches, and the request behind it. Each part makes assumptions about the next. Those assumptions are a better starting point for a migration than a list of files with the wrong extension.
Types should explain the application
TypeScript was already familiar to me, and at Spidertracks I migrated core parts of the JavaScript application to it while extending the frontend.
A gradual migration is a supported way to adopt the language. TypeScript can accept JavaScript files alongside TypeScript files through allowJs, which gives a project room to change incrementally. Its JavaScript migration guide covers that approach. The important architectural point is that a useful migration can start before every file has been converted.
I care most about types at the boundaries other code depends on: the values in state, the actions changing them, the props a component accepts, and the data expected from a request. A type that explains one of those boundaries helps a future change reach the right places.
There is a trap here. It is possible to make the compiler quiet without making the application easier to understand. Broad assertions and any can preserve an unclear contract almost perfectly. The file is now TypeScript, but the next engineer still has to discover the same assumptions.
I prefer to let a type answer a question. Can this value be missing? Does a loading state also contain data? Are these two identifiers interchangeable? If the type can't answer, it is worth finding out what the application actually expects.
React and Redux share the migration
A React and Redux application makes those boundaries particularly visible. A component reads some state; an action changes it; other components may depend on the result. Converting just the component leaves the assumptions in the surrounding flow untouched.
That doesn't mean every conversion needs a broad refactor. It means the work has to account for the path the data takes. I would rather make one path comprehensible than convert many unrelated files and leave their relationships vague.
Types also make future work more direct. When an action or a state shape changes, the compiler can identify code that no longer matches the declared contract. It can't decide whether that contract is correct, and it can't check a server response simply because a client has assigned it a type. Those remain engineering responsibilities.
Replace an endpoint, preserve its contract
On the backend, I built Node.js endpoints using Express to replace existing Spring-based services. A frontend calling an endpoint doesn't need to know which framework implements it, provided the behavior it depends on remains available.
That makes an endpoint a useful boundary for incremental change. Its method and path are only the beginning. Request validation, response shape, permissions, error status, and side effects all contribute to the contract. Express's routing guide explains the mechanics of matching methods and paths to handlers; the application still has to define everything the handler promises.
For a replacement, I would look beyond the successful request. What happens when a value is missing, a caller lacks permission, or a dependency returns an error? Those cases are part of what an existing client knows how to handle. Matching the happy path alone leaves a surprisingly large portion of the API undefined.
Moving from Java to Node.js is a change in implementation. A clear contract lets that change stay local to the endpoint instead of spreading through the frontend at the same time.
Choose changes that can be understood
The appeal of this work is its scale. Modernisation can happen in units an engineer can understand and a reviewer can assess: a data flow, a component boundary, an endpoint.
There will still be a period where old and new code coexist. That is a normal consequence of changing an application while it continues to grow. I want the boundary between them to be obvious, with enough clarity that the next piece can follow the same direction.
At Spidertracks, the work was extending the application while improving parts of the foundations beneath it. That is still how I like to approach legacy systems: find a useful boundary, understand its behavior, and make the next change easier to reason about.