On serving JavaScript applications
07 Mar 2017

For some time all the side projects I’ve been working on are based in the cloud, on either Amazon Web Services or Google Cloud. Both of them offer similar products in terms of what I usually. Also, their core cloud components are not that different, with AWS having way more extras. Despite the increase productivity when utilizing cloud, I noticed there is one thing I waste countless hours on. Crafting user interface and setting it up as a separate application takes definitely too much time for me. If my statement raised your eyebrow, let me explain what exactly my problem is.

First, let me describe how I usually structure my application. I try to keep everything as minimalistic as possible, structuring every piece as microservice and making all of them cooperating with each other. And by “microservice” I mean even services as small as two dozens of lines of code (Go programming language helps a lot in achieving that goal). Following this principle, a frontend JavaScript application fits into the definition of a microservice perfectly - all in all it’s just another service communicating with other ones, even though it’s running in remote location, which is user’s browser.

Apart from being perfectly logical and tingling my architectural sense of beauty, this approach has lots of benefits. JavaScript application becomes an entity on its own, living in it’s own repository, with own build tool and set of dependencies. You can build your frontend pipeline to be dedicated to the actual user interface, and the backend applications don’t have to know anything about what consumes them. Another benefit is that your frontend application becomes just one of the consumers of you API, no different from a mobile client or external consumer.

I’ve worked with couple of projects that mixed rich JavaScript interfaces with other server solutions in the same source tree, namely projects created in popular frameworks like Django or Ruby on Rails. These solutions allow developers to create working prototypes incredibly, but they couple the browser code tightly to backend code. So when we started the projects, we were able to create something of value very fast, but when the application grew it was increasingly harder for UI guys to work on that, and for the more backend oriented developers to modify something without breaking the UI.

Another benefit of a separate JavaScript project is the ease of deployment. After all, the output of building a JavaScript interface application is just a bunch of files served by HTTP server. In the cloud solutions this space is perfectly filled by Amazon S3 and Google Cloud Storage, offering static web hosting. If you want to be fancy, you can even distribute across different regions pretty easily with Amazon CloudFront CDN. When you have larger budget, you can even go for dedicated solutions like Firebase or Aerobatic or go with external CDNs to make latency even shorter.

Unfortunately, even though the benefits of having a separate interface are theoretically superior to downsides, in practice the latter can cause a huge pain. The reason is an application running in a browser is not the same as application running on a server. It actually lives in a sandbox environment with heavy security restrictions, which make interacting with it an obstacle course.

The problem I have to handle the most often is Cross-Origin Resource Sharing, a.k.a. CORS. This is a mechanism implemented in all the browsers that ensures that your HTTP calls to external services are actually allowed. Before making an actual GET, POST or DELETE operation to a service, browser makes an OPTION request, asking whether the current domain is actually allowed to make REST calls from the browser to the service. It’s rather easy to understand how it works, and all the HTTP frameworks and toolkits provide at least some integration, but I tend to forget about it when first deploying my backend services. Then, when I deploy frontend I have to go back to the target service and add the CORS layer, just for the compatibility. It’s an additional step that doesn’t provide any value for the users of my service, adding one more thing on a list of stuff to remember.

Another problem is the lack of SSL encryption by default in S3 and Cloud Storage, which are the most convenient way to serve these applications. In Amazon Web Services you can overcome that limitation with pushing your S3 directory to CloudFront CDN, but it’s still one more step to handle. Especially when you have to remember to invalidate the old version when you update the application. Alternatives like hosting your own HTTP server make no sense - my only requirement is serving files, I don’t want to care about hosting and scaling another service. Again, these solutions add more complexity and require more actions of me, without providing any particular benefit for the customer.

I found two decent solutions to the mentioned problems. The more straightforward one is using a separate application in your architecture that will be used as a kind of HTTP proxy. With this approach you can still have all the benefits of a separate JavaScript application, and still not have to worry too much about the CORS. You still have problems with SSL though, so it’s not perfect. On top of that, you have another service to take care of, and adding new feature in UI will likely require adding new proxy in the service.

The other way and the trade-off I decided to take is setting up an Express.js application in my frontend. This is the best of two worlds - I still can develop in JavaScript and use the tools dedicated in the language, without the need of switching context. At the same time, I don’t have to worry about CORS at all. The downside here is that I can’t deploy to S3 anymore. I can still deploy the application to App Engine, which has an option to serve static files.

The solution is not perfect, but it does it’s job. I’m still thinking about better solution, but at this moment I am not sure whether I will be able to come up with something that won’t be a dedicated service.

comments powered by Disqus