Project Architecture

I’m going to go over a few of the points of my project architecture as it currently exists - this won’t include any token based authorization (which I’m in the process of adding). Click here to view my github repo and see the project as it currently exists.

index.js

The server is set up with the following files - index.js, handlers.js, data.js, config.js and helpers.js.

We begin by exporting the correct config object from our config.js based on the environment specified - the code defaults to staging unless PRODUCTION is specified as the process.env.NODE_ENV parameter when starting the survey. The config object contains some other properties to be discussed later - the important part for now is that based on production or staging environments, we have a specific port our server listens on.

Once a request comes in at the port we’re on (pre-deployment here), our server receives it and passes it along to a server logic function.

That function uses the Node.js URL module to parse the request object

  • It first grabs the pathname and trim it. We use this object later to check that our router has the specified path.

  • Next, it gets the query string (for GET requests, since our POST requests will be coming in the request body).

  • Next, it gets the HTTP request method (GET, POST, PUT, DELETE).

  • Finally, it gets the request headers (content-type, for example).

The same server logic function then handles collecting body data (for POST and PUT requests) using an event emitter (req.on). It currently uses a string decoder to append data to an empty string but this can likely be changed by preemptively specifying utf-8 encoding.

Once the request data has been collected, we implement our routing logic with a small router object. This is where the trimmed pathname comes in to play - if the path exists in our router object, we assign that to a handler variable. If it doesn’t exist, we deal with a “not found” scenario.

Our handler variable now contains a reference to a handler function name in our handlers.js file - we then create an object using the information gathered from the request object - this includes parsing our gathered data string to an object using a helper function from our helpers.js file.

Our handler variable (which is a function) calls the appropriate function in our handler.js file and passes along our data object (containing the information gathered from the client request) and will receive a status code and a payload from a callback.

data.js

Before we dive into the handlers.js file, we need to look at the data storage functions we have here. These are what ultimately get called by the handler functions once they extract the appropriate data from the client request data object sent from the index.js file.

It’s important to note that we have a folder within our project directory called “.data” - this is where we store our data. Additionally, that folder has subfolders such as “token” and “users” for specific pieces of data.

Our data object that we export from the data.js file has four functions - create, read, update and delete. These all correlate to handler functions that work with CRUD functionality. This project currently only write to local file stores (soon to be updated to use MongoDB or a hosted PostgreSQL DB - I haven’t decided on which) - as such, the first thing the data.js file does is grab the current project directory. We also create a .data file to store data files (one per user) and add that on to the project directory filepath so that we can write files to it.

The create function takes a directory, filename to create, data object and a callback (which returns an error and data - data in the event that we’re reading a file and need to return the contents). We open the file using the project direcotry, file directory and concatenating the filename and “.json” and pass the appropriate I/O flags - they allow for writing to the file and also create the file if it doesn’t exist. If there is no error, we have a file descriptor ready to use.

We take the date object to write (which is a javascript object), stringify it and call fs.write, passing along our file descriptor and the stringified data object. If there is no error, we close the file. We also handle all possible errors sent from callbacks along the way.

The read function accepts as parameters a directory, a filename and sends a callback to the handler that invokes it (responding with an error and the data we’re reading from the file system). The subfunction in the handlers.js file that calls the data.read function passes along a data object containing identifying data used to find the right file to read from (we’re using a phone number in this case). Just as with the create function, we use the node fs module to read the file using the project directory + file directory + filename passed + “.json” - if there is no error and there IS data from the callback, we parse the JSON to an object and include it in the callback.

The update function accepts the exact same parameters as the create function - we need to check that the file exists so we first open it. If it exists, we truncate the file using the file descriptor passed from opening it - if there is no error, we use fs.writeFile and stringify the data before passing it to the file. Then we close the file and we’re all done.

Our delete function takes the same parameters as our read function (since, at its core, it performs the same task of reading the file). We open the file using out project directory variable + directory + filename + “.json” - we pass that to fs.unlink and if there is no error, we callback “false” as an error.

handlers.js

Our entire file is a large exported function with subfunctions - this is similar to our data.js file. Our file has several objects - a handlers object (which is what we export) and a handlers.users object which houses all of our route handlers. This is somewhat encapsulation - files importing our handlers.js file only have access to the handlers object, not the handlers._users property of handlers (which itself is an object). We access handlers.users FROM the handlers object. To illustrate this point :

var handlers = {}
// our index.js router is going to hit this function here but has NO knowledge of handlers._users
handlers.users = function (data, callback) {
    var acceptedMethods = [‘get’, ‘post’, ‘put’, ‘delete’]
    var requestMethod = data.method.toLowerCase()
    if (acceptedMethods.indexOf(requestMethod) > -1) {
        handlers._users.requestMethod (data, callback)
    }  else { callback (405) 
    }
}
var handlers._users = {}
var handlers._users.post = function (data, callback) { // logic here…}
module.exports = handlers

Here’s what the handler object that is exported would look like as an object literal -

var handlers = {
    users : function (data, callback) { // put request method validation code here} , 
    _users: { 
                 post = function (data, callback) { // post route logic here } ,
                 get = function (data, callback) { // get route logic here } ,
         }
}

The users property of the handlers object is itself an object - however, that is abstracted away from our index.js file which only calls the handlers.users method (which in turn calls the requisite handlers._users method.

The post function is passed data and a callback method from our index.js file - we’ll return a status code and a payload object (an error, if we have one) back to the calling function in index.js. The data object, remember, has a querystring or a payload we collected using an event listener on the client request object.

It’s important to note that we’re going to consistently use an inputted phone number as our FILENAME - this will be used for all lookups. We have several required fields that must be present in our data payload in order to create a user - phone number, first name, last name, password and tosAgreement. We extract all of those values from data.payload - if we have successfully extracted ALL of them, we can continue. First we sanity check to see if the user exists - we call data.read and pass in the phone number as our filename and “users” as our file directory. IF THERE IS AN ERROR, THAT MEANS THE USER DOES NOT EXIST - we may proceed with creating the user.

First we need to hash our password - we create a hash function in our helpers.js file that hashes our user password. You can use an HMAC and store the hash key in the config.js file (add it to your .gitignore directory) or just use a SHA256 hash without a hash key. We sanity check the length and type of the object to be hashed and then return the hash. Back in our post function, within our call to data.read, if we were successfully returned a hashed password, we build an object to store on file. It contains the phone number, first name, last name, tosAgreement and the HASHED password - we then call data.create, pass in the users directory and the extracted phone number as the filename and pass in our object we created. We then handle all err first callback responses accordingly and we ourselves return 200 as the statusCode in our callback to the index.js handler function.

Our get function accepts data and a callback - as with all other handler functions, we’re going to call back a status code and a payload (which is an optional error object). We are looking for the query string here in the URL since we aren’t doing anything with data collection from the request object - we need to extract the phone number from data.query.phone. This SHOULD be the same as the filename in our .data/users folder so we can use that for lookup. If we successfully got the phone number from the query, we initiate a call to data.read and pass in the users directory and the phone number as the filename. If there is no error and there is data, we can remove the hashed password from the data object before returning it in a callback along with a 200 status code.

Our put function is a little bit more involved in terms of logic - we only want to overwrite fields that need to be changed. While we’re not going to compare new values to the original ones, we can always add in later. As it stands, if a user submitted a new first name as “Karan” and the first name on file is “Karan”, we’ll still replace it. A phone number is a required field here - we need it to read the file contents. If we managed to extract a phone number, we see if there is a first name, last name or password to update. If ANY of those fields have been sent in the request body, we need to change them. We call data.read and pass in our phone number as the filename - if there is no error and we ARE returned a data object, we replace the data objects property values with our new ones if they have been included in the request body. We then call data.update and pass along our new data object and proceed to handle all err first callback cases.

Our delete function is very straightforward - we take in data and return a callback as with all other handlers methods thus far. We check the data query for a phone number - if we manage to extract it, we read the user’s file to make sure they exist. If we can read the file, we then call data.delete and handle all err first callback cases.

That’s it so far! Follow up posts will include more writing and code samples for the next set of features to add in - token based authentication, building out a simple front end with vanilla Javascript and (probably) replacing all callbacks with async/await since it comes standard out of the box with the new V8 engine now.