Topic 9: Moving towards Modular Express Servers: Routing and Organising Code in Modules

In this topic we will start to look at how we can make our code more modular by writing routers and also how we can write middleware in modules. Specifically we will look at:

Express Routers

Having looked at modules, we will now look at how to implement an Express router as a module.

Implementing an Express Router as a Module

In a larger application, you will quickly find that your main Express file (often app.mjs) will become very large, handling a large number of routes. What you can do instead is to define a Router. A router allows you to set up a group of routes which match a particular path. For example we might create a router matching the path /products which will handle any routes beginning with /products, e.g: /products/all, /products/id/:id and so on. To do this we create a routes folder within the folder containing our main Express server, and place a route handler in there, which would be saved as a Node module (we looked at modules in topic 5. Here is an example router module (product.mjs) which we would save in our routes folder:

// routes/product.mjs 

import express from 'express';
const productRouter = express.Router();

productRouter.get('/all', (req,res)=> {
    // code to return all products
});

productRouter.get('/id/:id', (req,res)=> {
    // code to find the product with the given ID
});

export default productRouter; // export the module for external use
Note how we are making productRouter the default export from this module, which means we'll be able to import it as follows:
import productRouter from './routes/product.mjs';
This is shown in the full Express application, below:
import express from 'express';
const app = express();

// Import our product router module which we created above
import productRouter from './routes/product.mjs';

// Tell the app to use productRouter for all routes beginning with /products
// As we are using "use()", the router is acting as a middleware - see below
app.use('/products', productRouter);
app.listen(3000);
So the song router is imported from the routes/product module (corresponding to routes/product.mjs) and then we call use() with two arguments: So, the route /products/all will be handled via the product router (as the route begins with /products) and then via the /all route within the products router.

Further Middleware

Last time you were introduced to middleware. However, we wrote our middleware in our main app.mjs which is not ideal for reusability. We saw examples last week of standard middleware such as express.json() so it is valuable to write middleware in its own modules so that it can be imported into other projects. As an example we will look at a piece of CORS middleware.

Here is a piece of middleware which adds a CORS header to all responses to allow clients from any domain to connect. In this example, the middleware is written directly inside app.mjs.

app.use((req, res, next) => {
    res.set('Access-Control-Allow-Origin', '*');
    next();
});
Note how we specify a custom arrow function as the argument to use(). This is our middleware, and you can see that it takes req, res and next parameters as described above. Note how our midleware adds the CORS Access-Control-Allow-Origin header to the response object to allow any domain to connect, and then calls the next function in the middleware chain with next().

However, we can write the middleware as a separate function, in a module, as long as it has the required req, res and next parameters. As we have seen, the advantage of this method is that your function can be placed inside a module and imported into other projects. Here is an example of writing our CORS middleware as a separate function in a module:

// corsMiddleware.mjs
function corsMiddleware(req, res, next) {
    res.set('Access-Control-Allow-Origin', '*');
    next();
}

export default corsMiddleware;
We could then import this from our main server, and use() it, as follows:
import express from 'express';
const app = express();

// Require the middleware module
import corsMiddleware from './corsMiddleware.mjs';

// use() it
app.use(corsMiddleware);

// Routes
app.get('/', ...);

// etc

app.listen(3000);

dotenv - Environment variables

Finally in this topic we will cover a useful Node module, dotenv. The dotenv module allows developers to specify environment variables - variables which can be set by the user as input to the application and may change - within a file, .env. Examples of such variables might include database name, username and password. This also has a security advantage: by adding private data to your .env it means that you can keep information such as passwords outside your source code, and publish your code to GitHub or another repository without giving away such information.

dotenv is quite easy to use. Firsst of all, as it's a third-party module, you have to install it:

npm install dotenv
You then import it, for example below we import dotenv after we have initialised our Express app object.
import express from 'express';
const app = express();
import 'dotenv/config';
Environment variables are placed within the .env file as a series of key/value pairs. For example, here is a .env file containing database settings:
DB_HOST=localhost
DB_USER=root
DB_DBASE=mysql
We could then use these in our server via the process.env object. So we could use process.env.DB_USER to access the database user, process.env.DB_HOST to access the machine running the database, and process.env.DB_DBASE to access the database name. For example, if we were using MySQL, which requires a host, username and password, we could do:
const con = mysql.createConnection({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    database: process.env.DB_DBASE
});
It's important to understand that you can only use process.env after you have imported dotenv, as shown above.


Finally - creating a module to initialise the database

One problem with our code so far is that we have had to initialise and load the database multiple times, once in the main app, and once per router. This is clearly inefficient and wasteful. What we can do instead is to create a module to initialse and export the database connection:

// db.mjs - initialsie and export the database connection object
import Database from 'better-sqlite3';
const db = new Database("wadsongs.db");
export default db;
As the database object is exported, we could then import it from every file which needs to use it, either the main application or any router:
import db from './db.mjs';
or, from a router:
import db from '../db.mjs';

Exercise

The exercise will allow you to practise with routers, middleware and dotenv.

IMPORTANT: This is a slightly more advanced topic, so you need to ensure you have completed the sessions and logins exercise from the previous topic.

  1. Make a copy of your Express server from last week, so you still have the original version for reference.
  2. Create a separate router file, containing routes for users (login, logout). This should be named users.mjs, should create an express.Router() as shown in the second example, and should contain the three routes (/login POST, /login GET and /logout).
  3. Include the router you have just created in your main Express app under the top-level route /users, as shown in the example.
  4. As shown in the example with CORS, write the user-checking middleware (from last week) in a separate module as a function and export it. Then, import it and use() it in the main server file by specifying the name of the function exported from the module.
  5. Test it out.
  6. Now, similarly, move all your routes to handle songs (i.e. everything you did in week 2) to a separate router inside the file songs.mjs. In the same way that you did for your users router, include this router under the top-level route /songs, and test it by searching for all songs by a particular artist by requesting the correct URL in your browser.
  7. Install dotenv and import it in your main server application. Store the database name (e.g. wadsongs.db) in a .env file and alter your code so that it reads the database name from .env.