Building my own NodeJS MVC Framework — Part 3

Leonardo Pereira
10 min readMar 29, 2020

--

The framework and the server. Real world coding!

Before starting this, let's add all packages we're going to need. I'll explain all about them later as we use.

So, run in your root directory the following command: npm install esm ejs node-cookie node-static path-to-regexp string_decoder --save

Add 3 new files in the src/ folder: Framework.js BaseController.js and Router.js . Also add a new folder src/middlewares/ .

With this done, let's use the power of esm to translate our import/export correctly, since some of the packages only responds to require() and I actually want to use fresh new ES6 code.

Open our package.json and our "main": "main.js" should be as this and in scripts add "start": "node -r esm index.js" below "test" . This will tell npm start to use esm correctly. If you run npm start you get a MODULE_NOT_FOUND error. This is because we're missing 2 files in our root folder, index.js and main.js . Please create both of them and open index.js . The code will be just instatiating esm to run:

And in our main.js put the code as:

Run npm start again and you'll see it's still failing. But this time is Framework that doesn't have run. esm is working fine and will save a lot of our time. Open src/Framework.js and let's write our first server response, using the port 4001 as in the code, but you can change it to anything you want.

Paste the code above inside Framework.js and open http://localhost:4001, you'll a welcome message Hello, world! . Our server is up. :)
Before explaining what we did, let me say we're not using any kind of hot-reload, this means we'll have to shutdown (ctrl+c or cmd+c) in our terminal and start the server again npm start to see our changes (just our template files will be reloaded without needing restarting the server). Hot Reload is a different topic which I won't be covering, and although this can be tiresome, it'll not hold us to achieve a simple but functional MVC framework.

Let's see what we did so far. First we imported 4 packages will be using along in the Framework.js, we are already using 3 of them, but fs is unused for now. http is the default server NodeJS provides, our main tool to tell the browser what we want to render. ejs is the template I've choose to work in our views, will be extremely useful for a Rails-Alike framework in NodeJS. And the last one is nodeStatic . It's a really useful package to render static data, such as images, stylesheets, static html and much more. Why I have choose to use nodeStatic instead of building or own handler? Because it has a lot of small details we'd need to take care for a safe and useful static provider. Rendering a single file is pretty easy, but we can fall down in security issues, someone can ask for a file inside our server and a nodeStatic handles the security. Also, nodeStatic tells the browser what to use from memory or not, handling caching and making our entire experience way more smooth.
We're focusing in the framework, but we can use tools to help us more and more, and we will.

In the constructor() we're having just one argument, the Router, we aren't using it now but we'll very soon. For the variables our framework will need to know, we have 4 "constants", _http (our server), _engine (ejs), _router (our argument) and _static (nodeStatic handler, at ./public ). For the framework data, for now we have just 3. _controllers _middlewares and _staticRoutes , self explanatory, the _staticRoutes will be used later to point our images, stylesheets and javascripts correctly. We want to keep it very tied.
At least, our framework helpers, we're only going to use this.logger for now, is just a more prettier message to inform what is happening.

In the run() we create the server, always send request and response , since we're going to use both a lot. We just print a simple Hello, world! response. server.listen(port, hostname) is responsible for starting everything.

Which this little we have a server running. We can apply every logic we want inside the createServer, and we will, making the framework functional.

Now, we'll move to the Router. But before, let's try a few things, type different urls and see the logs are changing:
- http://localhost:4001/posts
- http://localhost:4001/posts/1
- http://localhost:4001/something-else

As you changed your browser url, our server responded with the method GET and the url we have requested / or /posts . This is extremely important information for our Router. Also, the browser showed the same Hello, world! everytime. It's because although the url have changed we're still sending the same response.

Ok. Let's open our src/Router.js file and start our requests mapping.

This is a simple class with 4 public methods to correctly populate our this.routes hash. We're importing the path-to-regexp package but still not using, we'll use it later in the future resolve() method which will translate dynamic routes such as /post/:id and providing an id parameter to us to pass through our controller.
Also, we're exporting this as a new Router . Which means we import a constant instead of the class, allowing us to use Router.get() right away.

Now, update our main.js with some basic routes:

As you can see, the second parameter resolve is our controller and action to be handled. As ControllerClassName#action . Which in the future will call the action as a public method in our controller. We'll magic autoload all our controllers to our framework to be able to point all requests correctly.

Let's change our Router.js again and include our resolve() method, using match() to have smart routes. Include this method inside the class, below delete():

First we catch all routes in the current method we want, and we loop through everyone trying to match the path with the route we have. If we have a success result, we return a hash with {controller, action, params} . Handling the smart urls from path-to-regexp match() function.

If our loop didn't find anything, we return null as we're going to use as Not Found in the future.

With this in place, let's update our Framework.js to tell us if a route is a match or not. Add a new method below our run() :

And replace our hello world code inside run() withthis._resolveResponse(request, response); .

This will be handling our route matching, relying in our Router.resolve() logic . If the resolve response is null, we'll show a 404 not found message, if we found something, we'll just show which controller will be handling this logic. We've also included a elapsed time logger information, it helps a lot to show how fast a tiny framework can be.

Run npm start again and test your routes. It's amazing how much we can achieve with so little.
Now that our routing is in place, let's work with our controllers. But first let's autoload to our Framework.js all our controllers at ./controllers . It's easy to achieve when you know what you're doing.

At the end of our Framework.js class add a new async method:

This will handle loading everything from a folder, we're only using it for controllers, but if we want to change in the future we're welcome to.
So, inside the run() method, add this at the beginning: this._controllers = await this._autoLoad('controllers/'); .

Now, we'll have access to every controller inside our folder. This do not handle multilevel controllers so far, but it's out of scope. Ok, let's add our request encoding to UTF-8 and include a hash to be exchanged between our controller and future view holding valuable data. Our new run() method will be:

Now. Let's write the code for the BaseController.js it'll handle data to be exchanged between the views, the rendering logic (as we want to render 200, or perform a redirection) and the template path of our file. We'll be writing the run() method for the BaseController later, first let's structure what we want to have:

run() is the method that will be rendered by our framework to perform the controller correct action. Here we're introducing some ground and two extremely important methods to be used in our new controllers: this.set() and this.setG() as setGlobal.
In our controllers, we'll use as this.set({title: 'some title'}) to be able to access the variable in our templates later, as <%= title %> . setG is slightly different as it'll be available in all templates and inside views/layouts/application.ejs , which we'll create later. _localData will only be available in the template, but the globalData will be available in the template and the layout.

One really important private variable to pay attention is this._renderData . It's the new one responsible to tell our framework how to behave later.
Last but not least, we have our hellper _controllerPath() . Which will help us to find the correct view for the action we're running.

Now, let's write our run() code:

We need to treat the data carefully before running the controller action. First we verify if the controller have the method we're asking. If not, we'll tell the framework to run a 404 not found.
Second, we start a try{} catch{} block for error handling, and do a for loop through the data second parameter. Populating the controller with everything we pass from our framework. This means we'll include this.params to be accessible in the controller, and also allow form requests later with the middlewares.
Now we call our method as await . In case we have asynchronous operations, and we set the templatePath as we really want to present. We're almost there to render our view files into the browser.
Now, we want to do two more things.

First, let's create two controllers to be used in our project which will extend our BaseController.

controllers/HomeController.js

controllers/PostsController.js

Now, let's call the controller our Router have choose and perform it actions, you'll be able to check the server logs responding each path correctly.
Go to Framework.js and update _resolveResponse with this:

We instantiate our const controller and remap our request.controller.data.params extending with what we have from our Router. Finally, we call our action and let our BaseController do what we've told it to do. Restart your server and see how the pages responds now. With our ORM we're already able to deal with data. But before this let's move to our views.

Go back to Framework.js and include a new method below _resolveResponse
We're now using ejs to render our views :)

We also encapsulate the request time here, for debugging purposes to know how long it takes for ejs to parse the template. We set a default string to <template not found> . And call renderFile from ejs. renderFile provides a callback which we can use our data. We check if we get any error (like trying to open a template that doesn't exists). Reallocate our renderedHTML calculate our elapsed time and just return our string.

Now, let's create our views. Just create blank files in the views/ folder as listed below:

  • views/layouts/application.ejs
  • views/home/index.ejs
  • views/posts/index.ejs
  • views/posts/show.ejs

Populate views/layouts/application.ejs with the following code:

This is a simple base for a html page, the most important thing to notice is <%- template %> . template will be our yield holding our action view, and <%- %> is how ejs prints html without encoding, for safe reasons we will be using <%= %> most of the time.

Let's update our Framework.js _resolveResponse() method to render our layout and template as we want to. Replace the response.statusCode = 200 and the 2 lines below for rendering the success with this:

Here we are dealing with the _renderData from the controller, acknowledging what we really want to do, and we use our brand new_renderTemplate for parsing and storing as string both the routed template and our main layout. Exchanging all the bits we have, as localData and globalData.
Finally, we change our response to end with our layout. Restart your server and open your browser, now we can view our views.

Update your views/home/index.ejs and views/posts/index.ejs with any content you want. In your views/layouts/application.ejs add this before your <%- template %>

Save and open your server, you'll see our navigation is fully functional now. Also, our controller is now fully capable of integrating with our ORM. I hope you have a populated posts table, we're going to see our magic happening.

One small thing before moving along, update your package.json and inside start script prepend: NODE_ENV=dev

Open your controllers/PostsController.js and update it to:

Now, update your views/posts/index.ejs and let's show our data:

Restart your server (we changed our controller) and refresh the page, if you have persisted data you'll see the data. If not, just prepend Post.create() populating data in the controller action, restart the server, remove the code, add it again. Not fun by now, but we'll be handling our forms in the next part.

Let's update our view for PostsController#show as well:

Now you don't need to restart your server. Click in the post and see our data is being collect just fine.

Ok. Let's stop for a moment and see what we have and what we want to have. We can map our routes, call our controllers and actions, deal with a very basic 404 not found page, and we have a functional ORM. If you're like me, you were testing your framework with the console of your browser open, if so, we keep trying to load a stylesheets/main.css file and without success, we're getting a 404 everytime. That's because our Router doesn't know how to handle our main.css, but it should not know that. That will be a static route. Let's populate our main.css with some tiny code just to view a completely different screen and add our nodeStatic to the job.

Inside public/stylesheets/ create your main.css and include:

Now, open our main.js file at root level, and update with a new method we'll be including in a second:

Here, we add a new static(array) public method, it'll simply set our this._staticRoutes to our new value. This is what will allow our static method to map the routes we want to. Open your Framework.js file and let's include it.

Add it below our constructor() method and let's update our run() method:

Change yourrun() method as this one. If you notice what we have modified here, it's very simple, instead of calling this._resolveResponse(request, response); right away, we do a find and check if the path matches our request.url, as we request always starting with a / , our server sees /stylesheets/main.css , if stylesheets is in our static routes we allow nodeStatic (in our case is this._static to serve the file.

Restart your server and see that main.css is being loaded just fine. :)

In the next step, we'll be working with our middlewares and allowing us to create a new Post as he wants. And the power of <%- include(file) %> from ejs to easily couple our views.

Final Part.

--

--