Skip to main content

How to migrate from Sails.js to Express.js (or how to finally reach land)

·1078 words·6 mins
Sebastian Scheibe
Author
Sebastian Scheibe
Table of Contents

One of the projects I used to worked on, was a large one written in Sails.js. Now, after such a long time of development the project grew and grew and there was a need now for structural changes. Also it just became necessary to use a compiler for syntax checking.

So, we introduced TypeScript. This step was easy and could be quickly achieved with just a new Grunt task and some modifications at the folder structure.

There is just one thing in Sails.js which made TypeScript less powerful_._ It makes all controllers, services and models available to the GLOBAL variable. This limits the possibilities of TypeScript since Sails.js always expects

module.exports = {

}

to be set.

With that size of a code base, it just becomes necessary to rely on features like type checking and code completion from TypeScript.

For these features we needed to implement classes. Now, having classes and the Node.js standard export module.exports is not such an ideal combination.

A class looked like this:

So, after some try and error, it seemed like the keyword export would work for Sails.js and we could use our required features from TypeScript.

This concept worked for some time, but in the end we faced sometimes issues with functions not being defined, depending on how the instance was accessed, via GLOBAL or via an import of the file.

This brought me then to the idea of the removal of Sails.js and the implementation of Express.js in combination with a full class driven approach for our code base.

It had also another huge benefit.

We could finally group files into sub folders. This was not possible, since Sails.JS just reads the first layer of the folders it works with (services, controllers, models).

The migration guide
#

So, how did we migrate in the end?

After some research of what modules are needed, it was more a task of try and error to see if the application boots and the unit tests are still running. :)

Custom app.js and server.ts(js) file
#

So, first step is to create a custom entry file for the application.

First, we created a new app.js and a server.ts file. The server.ts was created somewhere in the source directory and being a TypeScript file it has the benefits of being checked by the compiler.

The app.js file in the root folder would just call the compiled version of the server.ts file to start the application.

The server.ts file would look like your average Express.js file except that you would add some extra in there, to make it work like Sails.js in the beginning.

Constructing the server file was the major part of the migration, in the end.

There are a couple of things that need to be done:

Global Sails.js object
#

Sails.js makes an object globally available which contains features like logging, config object, i18n.

To get the code up and running, it was the easiest to just simulate this behaviour:

Setup all the middleware

  • CSRF
  • CORS
  • Locals (translations)
  • wantsJSON (which has the same behaviour as Sails.js)
  • Skipper (file uploads)
  • Default response methods (res.ok() / res.serverError()/ …)

Routing and policies
#

In Sails.js the routing and the policies are both set up with files, not in the code itself. This makes the migration quite time intensive, if you need to rewrite every route and its policies into code for the Express.js router setup.

If the application is small, this would be not a huge problem. Our application though contains 700 REST routes and the equivalent amount of policies.

In the end, I ended up writing two parsers. One for the route setup, which would parse the routes.js and one for the policies, which would parse the policies.js file.

This had also the huge benefit of other developers could just proceed with their day to day development and extend these files all while I was modifying the core parts of the application. Business as usual could proceed.

Template engine
#

Sails.js uses by default the EJS template engine.

This gave me a bit trouble, since the default setup of EJS did not work from the beginning with the setup of our EJS templates. There was an issue with how we use sub templates.

After some experimentation I found that it works properly with the package express-ejs-layouts.

This was the setup in the server file:

Also the render methods needed to be changed.

Sails.js implements a behaviour which, based on the controller, detects the correct template file.

The migration would go from:

to:

What about the database layer?
#

Sails.js uses its own written database connector, Waterline.

This made it a bit more complex since Waterline is made to run inside of Sails.js. Now that there is no Sails.js anymore, how would you trigger the startup? The documentation of the Github page does not give a lot of information on how to work with Waterline in your own project.

After some debugging of the documentation I came up with a class which replicates the behaviour of Sails.js.

The startup method would be called during the Express.js startup.

I came up with this:

Also I made it possible to access the model via the import and have the functions of Waterline available (find()/remove()/save()/…).

A model can now look like this:

Socket.IO
#

Since we rely heavily on the socket implementation, we were in need to reimplement in nearly the same way.

To init the socket.io we first init the express server. The instance we get from the express server is then used to initiate an instance of socket.io.

Here we use the Redis adapter to keep multiple instances of our application synchronised.

The cookie parser is being used to, as it says, parse the cookies on the first connection from a browser.

After that the Socket.io instance gets started and as the final stage, there is some middleware applied to the Socket.io instance.

In the monitor you can listen on events which are coming in.

As you can see, this methods differs from the controller mapping approach of the Sails.js Socket.io implementation. It should not be too difficult to adapt to the Socket.io event listening approach.

Last words
#

I am very happy with how everything turned out and how it works.

The next step for the future would be the migrate away from Waterline and towards Mongoose.

I hope you had the patience to read until this point and it might be helpful for you.