Developing a real MVC application
Let's apply the theory in practice now and create an MVC file manager application using Express 4.x and Mongoose (an object modeling library for MongoDB). The application should allow users to register and log in and enable them to view, upload, and delete their files.
Bootstrapping a folder structure
We will start by creating the folder structure. First, we'll use the Express CLI tool in the terminal to create the boilerplate. Apart from the public, routes, and views folders, we also need to add folders for models, helpers (view helpers), files (the files uploaded by users will be stored in subfolders here), and lib (used for internal app libraries):
Installing NPM dependencies
By default, the CLI tool will create two dependencies in your package.json file—express and jade—but it won't install them, so we need to manually execute the following install command:
In addition to these two modules, we also need to install mongoose to interact with MongoDB, async for control flow, pwd to hash and compare passwords, connect-flash to store messages for the user (which are then cleared after being displayed), and connect-multiparty to handle file uploads. We can use the following shortcut to install the packages and have them declared in package.json at the same time if we call NPM with the –save flag:
Express 3.x came bundled with the Connect middleware, but that's not the case in the 4.x version, so we need to install them separately using the following command:
Note
The middleware libraries from Connect were extracted into their separate repos, so starting with Express 4.x, we need to install them separately. Read more about this topic on the Connect GitHub page at https://github.com/senchalabs/connect#middleware.
We can always check what modules are installed by entering the following command in the terminal at the root of our project:
That command will output a tree with the dependencies.
Setting up the configuration file
We can get as inventive as we want with the configuration parameters of a project, like have multiple subfolders based on the environment or hierarchical configuration, but for this simple application, it's enough to have a single config.json file. The configuration variables we need to define in this file are the MongoDB database URL, the application port, the session secret key, and its maximum age so that our file will look like the following code:
In the main file of the application, named app.js, we handle the view setup, load the middleware required for the project, connect to the database, and bind the Express application to a port. Later on, we modify this file to set up the route handling as well, but at the moment, the file contains the following code:
Note that the preceding app.js file contains the code for the database connection. Later on, we will need other database-related functions such as checking for failed data validation, duplicate keys, or other specific errors. We can group this logic into a separate file called db.js inside the lib folder and move the connection functionality there as well, as shown in the following code:
The routes folder will have a file for each controller (files.js, users.js, and sessions.js), another file for the application controller (main.js), and an index.js file that will export an object with the controllers as properties, so we don't have to require every single route in app.js.
The users.js file contains two functions: one to display the user registration page and another to create a user and its subfolder inside /files, as shown in the following code:
The sessions.js file handles user authentication and sign out as well as renders the login page. When the user logs in successfully, the username and userId properties are populated on the session object and deleted on sign out:
The files.js controller performs CRUD-type operations; it displays all the files or a specific file for the logged-in user and saves the files or deletes them. We use res.sendfile to display individual files because it automatically sets the correct content type and handles the streaming for us. Since the bodyParser middleware from Express was deprecated, we replaced it with connect-multiparty (a connect wrapper around the multiparty module), one of the recommended alternatives. Luckily, this module has an API similar to bodyParser, so we won't notice any differences. Check out the complete source code of files.js as follows:
The general routes used to require user authentication or other middleware that needs to be reused for different paths can be put inside main.js, as shown in the following code:
The index.js file is pretty simple; it just exports all the controllers into a single object so they're easier to require in the start script of our application:
Now that we have seen what the controllers look like, we can add them to our existing app.js file:
Note that we included the requireUserAuth route for all the URLs that need the user to be logged in, and that the multiparty middleware is added just for the URL assigned to file uploads (which would just slow the rest of the routes with no reason).
A similarity between all the controllers is that they tend to be slim and delegate the business logic to the models.
The application manages users and files, so we need to create models for both. Since the users will be saved to the database, we will work with Mongoose and create a new schema. The files will be saved to disk, so we will create a file prototype that we can reuse.
The file model is a class that takes the user ID and the filename as parameters in the constructor and sets the file path automatically. Some basic validation is performed before saving the file to ensure that it only contains letters, numbers, or the underscore character. Each file is persisted to disk in a folder named after userId (generated by Mongoose). The methods used to interact with the filesystem use the native Node.js fs module. The first part of the code is as follows:
The most interesting methods in this model are the ones used to save a file and get all the files that belong to a user. When uploading a file, the multiparty module saves it at a temporary location, and we need to move it to the user's folder. We solve this by piping readStream into writeStream and executing the callback on the close event of the latter. The method to save a file should look like the following:
The function that retrieves all the files of a user reads the directory to get the files, then it calls the getStats function in parallel for every file to get its stats, and finally, it executes the callback once everything is done. In case there is an error returned because the user's folder does not exist, we call the File.createFolder() method to create it:
The only things that we need to store in the database are the users, so the user.js file contains the Mongoose schema for the User model, field validation functions, and functions related to hashing and comparing passwords (for authentication). The following code contains the module dependencies along with the validation functions and schema declaration:
Since we don't store the password in plain text but use a salt and a hash instead, we cannot add password as a field on the schema (in order to enforce its validation rules) nor create a virtual setter for it (because the hashing function is asynchronous). Due to this, we need to create custom functions such as setPassword, saveWithPassword, and validateAll as shown in the following code:
The authentication function is pretty straightforward; it gets the username and then compares the hash stored in the database with the hash generated by the password, which is sent as a parameter:
The first thing to do here is to create a global layout for our application, since we want to reuse the header and footer and only customize the unique part of every web page. We use jade as the templating language, so in order to declare the extendable part of the layout, we use the block function. The layout.jade file will be created inside the views folder as follows:
Note
An interesting detail in the preceding code is that we override the method interpreted on the server side from POST to DELETE by passing a hidden field called _method. This functionality is provided by the methodOverride middleware of Express, which we included in the app.js file.
Sometimes, we need to use functions for date formatting and size formatting or as a link to use some parameters and other similar tasks. This is where view helpers come in handy. In our application, we want to display the size of the files in kilobytes, so we need to create a view helper that will convert the size of a file from bytes to kilobytes. We can replicate the same structure from the routes folder for the helpers as well, which means that we will have an index.js file that will export everything as an object. Besides this, we will only create the helper for the files at the moment, namely files.js, since that's all we need:
To make the view helpers accessible inside the view, we need to add another piece of code into our app.js main file after the view setup, as shown in the following line of code:
This will ensure that whatever is assigned to the locals property is globally accessible in every view file.
In the views folder, we create subfolders for files, sessions, and users. The sessions and users folders will contain a new.jade file, each with a form (user login and signup page). The biggest view file from the files subfolder is index.jade since it's the most important page of the application. The page will contain dynamic data such as the logged-in username or the number of files stored and other stuff such as an upload form and a dashboard with a list of files. The code for the index.jade file will look like the following:
Running the full application
We have not covered the JavaScript static files or stylesheets used by the application, but you can fill in the missing pieces by yourself as an exercise or just copy the example code provided with the book.
To run the application, you need to have Node and NPM installed and MongoDB up and running, and then execute the following commands in the terminal from the project root:
The first command will install all the dependencies and the second one will start the application. You can now visit http://localhost:3000/ and see the live demo!