If you’re part of an engineering team that is currently using PHP and thinking of moving to Node.js, this post is for you. We won’t go into the nitty-gritty of porting PHP to Node.js and there will be no basics of Node.js. We will cover our decision-making; a description of where we started; higher-level considerations on writing a Node.js server; and, finally, our deployment strategy.
At 1stdibs, we made the decision to move from Apache/PHP to Node.js & Express for five main reasons:
- Less Code
- Full JS Stack
- Developer Happiness
- Return on Investment
- Future Optimizations
Full JS Stack (& the benefits thereof)
Having your stack in one language is just simpler. For developers, the lack of context switching makes them happier and more productive. Then, there’s the added bonus of making the tooling simpler. Instead of the two package managers we were using, Composer and npm, we’d need only one. Composer is great, but our tooling and client-side dependencies are handled by npm, so we’ll always need npm. Once we finish deleting all the PHP, we’d be left with npm as our one and only package manager.
Return on Investment
Longer term, our plan is to break two monolithic applications into smaller apps that can be deployed individually. This will be easier using Node.js, Express and npm. Theoretically, we could do something similar with PHP (e.g. using Slim). But we wouldn’t get any of the benefits enumerated above, we just get a bunch more complexity: operations would be more complex with Apache/PHP and scaffolding becomes just… weird.
Picking a Framework
The PHP app that we eventually replaced with Node.js had a few key responsibilities:
- login and authorization
- routing and server-side templating (i.e. serving HTML)
- bootstrapping the client-side apps
- proxying to services (sidestepping CORS)
- serving static assets (JS, CSS, images)
This was the base functionality that we would need to replace.
We looked at lots of frameworks, but settled fairly quickly on Express (check out the spreadsheet we used to evaluate). Any framework that wasn’t sitting atop Express seemed like a gamble. Express is well-understood and very well-documented. Plus, it’s possible to find people with specific Express training.
We added a few core modules from kraken (express-enrouten for routing and lusca for security); plus i18n-node for internationalization and Swig for templating (swig has subsequently become abandoned – oops – the dangers of OSS).
We considered adopting kraken fully, but we were using Twig for server-side templates in PHP and switching to Swig was very fast and straight-forward. Plus, we didn’t like Dust nor the i18n support in kraken.
Write The Server
After we decided on a framework, it was time to write the server.
Using Apache and PHP, you don’t write a server per se. Apache is the server and PHP is the application. With Node.js the server and the application are one. When switching to Node.js from Apache/PHP it’s important to address functionality you may have taken for granted when using Apache. With Apache, you (or a system administrator) configure the server then, in the PHP application, you don’t really need to worry about certain things that Apache is handling for you. Node.js works differently.
serving static files
In Apache, serving files is, needless to say, core functionality. Not so with Node.js. You configure static file serving in the application. Fortunately, serving static files is simple, well-documented and built into Express.
Most basic Apache configurations give you access and error logs out of the box. With Node.js, once again, you’ll need configure this in the application. Luckily, there’s some great OSS packages that make this pretty simple. Morgan is a basic request logger that is simple to set-up and allows you to log to an output stream (e.g. stdout or a file). If you need to log to a database or have other (e.g. more advanced) logging needs, check out winston.
A base piece of functionality we needed was the ability to proxy-pass client-side AJAX requests to our services. We feel that proxying all service requests through the domain from which it originated is simpler than dealing with CORS headers. Plus, if you want to use webpack-dev-server via a proxy (as we are doing), you’ll need a way to handle that in your Node.js app. We found http-proxy a simple and reliable solution.
All the other stuff
Along with the considerations above, we had a bunch of other work to do. Our starting point was an MVC application built atop CodeIgniter (CI) serving a suite of single page apps. The bulk of our work was porting:
- CI controllers to Express route handlers and middleware (including login and authentication)
- templates from Twig to Swig (trivial)
- data access from services (in order to bootstrap the client single-page apps properly)
A key component missing from the list above is our CodeIgniter models. That’s because we didn’t need to rewrite our PHP models! FTW! We used the Backbone models that we had written for the client-side apps. Of course, we needed to extend Backbone.Model.sync so that it worked universally on the server and the client.
If you have an app of any size and you’re considering one big release, don’t do that. Roll out slowly in a staged deployment. We took weeks.
Some benefits of a staged deployment:
- minimizes surface area for bugs
- one to two engineers can handle the development as you’re dealing with a handful of routes and/or features per release
- provides minimal disruption to ongoing feature development and enhancements – new stuff can continue to be released (this can lead to duplicated efforts).
- if you do it properly, you’ll be able to rollback instantly to the previous server
How do you roll out slowly? Put a server in front of your servers. We used Nginx.
+----------+ http | |---> Apache/PHP request---->| Nginx | | |---> Node.js +----------+
This allows you a great amount of freedom as you can “turn on” a route at a time (and turn it off if things go sideways – as they did a couple of times for us). We also found it helpful to be able to turn on routes without doing a code deploy. This provided us with some wiggle-room around our once-weekly release schedule.
There is a downside. You’ll need to ensure that your client-side code works with both your old Apache/PHP server and your new Node.js server. This doesn’t sound horrible, but you may need to port non-optimal functionality from the old server to the new server. Just hold your nose when you do it (and file a ticket to clean up the tech debt).
From start to finish, it took about a year to make the entire transition. This may sound ridiculous, but that time-frame included the decision making process (we were in no hurry); writing a core framework atop Express that fit our own needs; porting everything; and the slow, staged rollout. And, remember, we had only 1-2 developers working on it – on a part-time basis – the entire time.
If you’re thinking about making the jump, consider carefully. How does it benefit your team, how does it benefit your larger organization? If you’re part of business organization, remember that the business will need to continue to function. You’ll need to balance important business goals with your engineering goals.
We’ve been very happy that we made the jump.