Clean Code
Coding For Portability
Tips for writing better apps that can be deployed anywhere
If you have written code for anything more than a ‘Hello, World!’ program, chances are that you or someone you know has been in the dreadful “it was working on my machine” or “it was just working a few minutes ago” situations.
This can be very irritating and would often require some time to debug and track down the source of the error, but fortunately, you can reduce your chances of running into such situations with the implementation of some patterns.
Compiled languages are dependent on hardware and the environment. Interpreted languages are a little more platform agnostic, but problems still do arise, causing apps to crash on deployments across different environments.
In this article, I’ll address some of the common things that can lead to such errors and solutions to make your apps more robust and platform agnostic.
Configuration
An app’s configuration is all those things that are likely to change across environments(development, staging, preproduction, production, etc).
These include database URLs, credentials, and handles to external services.
The config should be stored in environment variables, not directly in the app.
Environment variables are variables that are set outside the program, usually through some functionality built into the operating system. The PATH variable is probably the most popular and one of the most important environment variables, directing the shell where it should look for executables. To view the value of the PATH variable, you use the command `echo $PATH` in Linux and Mac OS and `echo %PATH%` in Windows.
Storing configuration this way allows the app to load the specific values from the environment on which it is deployed, reducing the chances of app crashes when the app tries to use the wrong environment-specific configuration like IP addresses and credentials.
Another approach is to store the configuration in a separate file that is not committed to version control. The environment-specific variables are then loaded from this file. We can do this in nodejs with a .env file and load them as environment variables with the dotenv npm package.
Hard-Coded Port Numbers
Whiles the solution to this problem is also easily environment variables, it’s so common that I felt it deserved a separate exposition. Most developers have some favorite port numbers that they tend to use in all their apps(guilty), and these port numbers are set directly in code. The problem arises when you try to deploy this app in an environment where that port number is unavailable because it is already been used by another app. This can lead to app crashes.
To avoid this, it is better to set/read this as an environment variable.
You can run into another port problem when you try to make a request from the frontend to the server-side using a port and that port number is different in the current environment. My suggestion is to always relative URLs when making requests from the client-side of a full-stack application.
File System Case Sensitivity
It’s fairly obvious in NodeJS that firstName and firstname are two completely different variables and will most likely contain dissimilar information. What might get a little tricky is that, if I created a module called requestStatus.js, the code below would work perfectly in windows. It would cause a crash in Linux.
let status = require(“requeststatus”);
status.doSomething();
I/O is not handled in the NodeJS main thread. It is done through a library called libuv. In this case, libuv accomplishes this by interfacing with the native operating system. The Windows operating system by default is case insensitive and I would not be able to create requeststatus.js and requestStatus.js in the same directory because they are both seen as the same file, hence requiring requeststatus.js would resolve to requestStatus.js without error.
Note that this happens within the operating system itself as the NTFS filesystem itself is case sensitive.
The same code would fail in Unix-like operating systems because of case sensitivity. Mac OS is different in this sense since, by default, it uses HFS+, a case-insensitive file system.
Matching cases in code and filenames can seem quite trivial, but it becomes more common with file extensions. Saving as image as logo.PNG and requiring it as logo.png would work on Windows, but running the same code on Linux would fail. The case remains the same for my-awesome-video.MP4 and my-awesome-video.mp4, index.HTML, and index.html.
You can easily fix this by sticking with a convention to use lowercase for all your filenames. If you don’t want to, just make sure you match the case of the filename in your code.
Payloads
Payloads might not qualify the general definition of config, but separating the payloads from the application logic itself will make it easy to move your app across environments and different versions of external APIs.
Keeping payloads in a dedicated file(s) makes it easy to spot and correct errors or swap them out in case there are some differences across versions. Payload properties often change when moving across environments and it’s easier to just change them in a separate file or swap out the file completely rather than having to go through your code to find where exactly a key was set.
Version Control
Using version control affords you so much flexibility including the opportunity to always roll back to the last working commit or version of your app in case there are some breaking changes in your codebase. This will save you some valuable time that you’d otherwise spend trying to track down the changes in your code causing a crash.
Dependencies
Beyond the fact that dependencies can get huge and I’m looking at a 400-megabyte node modules folder for a 300-kilobyte project as I am writing this, you should also not copy your node modules across systems or commit them to version control for portability and compatibility reasons. Dependencies include OS and environment-specific binaries and may lead your app to crash if you share them across environments. Some arguments can also be made for committing and sharing node modules including that, you can’t trust a package to stay on npm for the next five years for a project you will maintain for that long, but these can be mitigated with artifact managers such as nexus and artifactory. If you do decide to share or commit your node modules, bear in mind that there is no guarantee that they will work on deployment to another system.
Containerization
The immutability of containers offers another layer of protection, giving you the certainty that your app will be the same across environments. Also, most of the configuration is stored inside the docker container, reducing the work it takes to prepare an environment for an application. I strongly recommend using containerization if you have the opportunity to do so.
Ultimately, there is no crash-proof app, but at least, I do hope this article provides you with some food for thought in building platform-agnostic applications.