Re-writing a legacy web API takes time and the exercise will probably span across several releases. We will most likely be facing a scenario were both APIs run in parallel, having the old system servicing endpoints that haven't been migrated yet. But it turns out such an scenario can be achieved painlessly by using the good old http-proxy library and 404 responses! Here is how.
Personal statement
Legacy web APIs can feel excessively complex, are often built on top of discontinued technologies and are sometimes based on design principles that are not aligned with the evolution of the underlying programming language (e.g. callback hell vs promises). We developers dream of re-building such web APIs from scratch, using modern frameworks, with simplicity in mind and addressing all the blind spots that previous software engineers failed to perceive back in day. Oh boy, what a brilliant job we would do!
And, if we stick to a company for long enough, if we mention in the appropriate meetings how concerned we are for the security risks of relying on a discontinued framework, if we are lucky, we might witness a rare miracle: management giving the green light for a platform re-write.
Wonderful news! Let's assemble the engineers, pick the latest and most promising web framework, start drawing architecture diagrams, set overconfident time estimates and maybe even hire a new person or two. This re-write will be finalized in a matter of months 👏
But then, even if we could re-write the system that fast, would we dare to execute a full transition to the new infrastructure? The legacy system might have its flaws but it works and it's well tested. It has matured over the years. Can we guarantee that the new platform dots all the i's and crosses all the t's?
Let's be cautious for once. Finding a way to integrate the new platform with the old one would allow us to transition to the new platform gradually, validating the new software progressively and not losing any features along the way. Here is a transparent way of introducing a new server to the equation while keeping the old server fully functional.
The meat on the bone
Let's take the following sample Express web API. For the sake of simplicity it only has two endpoints; a POST endpoint that consumes a JSON body and a GET endpoint that can return 404.
Let's now assume the time has arrived to migrate the server to a newer platform 💃 Express is a perfectly solid framework but there are other frameworks out there which add other functionality on top of it and which happen to support Typescript out of the box. Frameworks such as NestJS. Here is what an empty NestJS server would look like.
From now on the NestJS server will be the new front door and it will forward requests to endpoints that haven't been migrated yet to the express server. Let's use docker-compose to launch the new infrastructure and then fire a sample request via curl:
{"message":"Cannot GET /get-endpoint/id","error":"Not Found","statusCode":404}
At this point all requests result in 404. What we need to do next is implementing the forwarding to the express server on the NestJS server. We can do so using a NestJS mechanism called Filter (to intercept NotFoundException errors) and the http-proxy library (to forward the corresponding requests to the express server):
Hello world
Before moving on pay attention to the change in the creation of the Nest application; we need to explicitly disable the parsing of the requests body (i.e. { bodyParser: false }). This is necessary because we need the original raw body when forwarding requests to the express server; otherwise express will error out when trying to parse it again. We will need to remember this when adding endpoints that need to parse the body, and add explicit parsing for each endpoint/controller.
Something broke
Ok then! We have a new server in place that forwards requests to the already existing server. This can safely be deployed to production and all web API calls should continue to work correctly (ignoring SSL and IP addresses configurations here). From this point onwards it's all about gradually migrating the old endpoints to the new platform.
Let's start by migrating the POST endpoint. Should be simple enough. Remember we disabled the default body parsing earlier so the requests' body will be empty by default. We will need to install the body-parser library and configure the corresponding Nest module to use it:
Hello world. {"property":"value"}
And let's finally migrate the GET endpoint. It is a simple endpoint too and it can implemented in a variety of ways:
Hello world
{"statusCode":404,"message":"Not Found"}
Another common way of implementing the endpoint in NestJS would be to use the NotFoundException. Note however that using such class would have an unintended consequence. Because the NotFoundFilter we implemented earlier catches NotFoundException and proxies them to the express server, the 404 returned by the NestJS endpoint would be treated as a not implemented endpoint. This would make the request slower and increase the CPU usage for no good reason.
Not Found
Fantastic! We have completed the migration and the express server can now be safely decommissioned 🎉 I wish all web API migrations were this simple. Before you go here is a Git repository with the code above and one suggestion learned from experience; adding a custom response header on each server will help you identifying which server is servicing each request.