Creating an Express Router

In the previous weeks, when building app.js, we had all of the code in the same file.

That can be fine for shorter files, but the longer the file gets, the harder it is to navigate in it. This is where the module Router comes in which is built into express.

The Basic Pattern

In a new file, that in this instance we call replRouter.js, we import a named export, from the express module. Since we don't need the full express framework in the file, we can get on by just calling that single export.

The Router() hold the same methods as the Express() for route handling, so we only need to change from app.get to router.get. Lastly, we need to export router, so we can collect the route handling in a different module.

// Basic Express Router pattern from replRouter.js
import { Router } from 'express';

const router = Router();

router.get('/hello', (req, res) => {
    res.send({ data: 'Hello :D' });
})

router.post('/api/repl', (req, res) => {
    const {replCode, sandBoxID } = req.body;

    if (!replCode) {
        return res.status(400).send({
            errorMessage: 'Missing the key replCode in the JSON body'
        })
    }

    // Process request...
    res.send({ data: { success, output, result } });
})

export default router;

Hooking It Up in app.js

Now that we have generated the router file, we need to import it into a app.js file. So that when we start the app, it will have access to the same routes as before.

Now app.js is much cleaner and is easier to navigate as well.

import express from 'express';

const app = express();
app.use(express.static('public'));
app.use(express.json());

import pagesRouter from './routers/pagesRouter.js'
app.use(pagesRouter);

import replRouter from './routers/replRouter.js';
app.use(replRouter);

const PORT = process.env.PORT || 8080;

const server = app.listen(PORT, (error) => {
  if (error) {
    console.log('Could not start the server on', error.message);
  };

  console.log('Server running on ', PORT);
});

Custom Template Engine

By utilizing a Template Engine, we can reduce the amount of boilerplate code by a massive amount.

When we think about it, the parts that can contain the most repetitive code in these types of apps, are not the business logic, but the frontend; css, navbars, footers, stylesheets etc.

So by splitting the HTML into reusable parts, we can define each piece once and compose them into any page, rather than duplicating the same pieces across every page separately.

How It Works

We need two files, templatingEngine.js and pagesUtil.js.

templatingEngine.js is for building the actual pages, based on options and the two files we always want to incorporate: header and footer.

// Templating example from templatingEngine.js
export function constructPage(page, options = {}) {
    const header = readPage
    ('./public/components/header/header.html');
    const footer = readPage
    ('./public/components/footer/footer.html');

    return header
            .replace('$$DOCUMENT_TITLE$$', options.documentTitle || 'Online Node.js REPL')
            .replace('$$CSS_LINKS$$', options.cssLinks || '<link rel="stylesheet" href="/pages/frontend/frontend.css">')
          + page
          + footer;
}

pagesUtil.js is for creating the pages and this is where we also parse the options, so we can define which CSS file we want to incorporate and so on.

One File Per Responsibility

There is a structure beginning to form: we want to make sure each file one serves one responsibility. This way our code is much easier for other people to read and navigate, but also, more importantly, to ourselves.

We used to have a very long app.js file, containing all of the pages routing and API methods.

However, this quickly turned out to being a very long file, containing many different things that could be split up into other files, such as pagesRouter.js and in our Online Node Repl replRouter containing the API logic for the REPL.

// app.js
import express from 'express';
import pagesRouter from './routers/pagesRouter.js';

const app = express();
app.use(express.static('public'));
app.use(pagesRouter);

Folder Structure

Code Placement and Efficiency

Making sure your know when your code gets executed, is quite important - when making these types of webapps, ask yourself if the code should run on startup or per request. This is something where we can make our code much more efficient.

Startup vs. Per Request

One import thing to run on start up, could be the pages. If they are built on startup, the user does not need to wait too long for the page to load. For this type of small app, it might not matter too much, but navigation does feel snappier.

We did this in class by generating a pagesUtil, that will generate the pages based on different options and through a homebrewed templating engine (We will get to that part later).

// pagesUtil.js
const week7 = readPage('./public/pages/week-7.html');
const week7Page = constructPage(week7, {
  documentTitle: 'VSS - Week 7',
  cssLinks: weekCssLinks,
  homeButton: homeButton,
  scripts: weekScripts
});

Files: Sync vs. Async

When serving pages with 'fs', we have a choice between fetching the file synchronous or asynchronous. Choosing one over the other matters, since we want to be sure the whole page is served when it is requested.

If files were read on every request, rather than once at startup, using synchronous would halt the whole server for each user and that's where async becomes necessary.

Why We Use Synchronous

The server needs the HTML content before it can send a response, so if it didn't wait for the file to finish being read, it would serve either a broken file or nothing.

// Synchronous example from templatingEngine.js
export function readPage(path) {
    return fs.readFileSync(path).toString();
}

SSR vs. CSR

Server Side Rendering as opposed to Client Side Rendering, is a question of who should have the authority to render pages to the user. There are advantages to both, just like there a disadvantages. I wil try to go through some of the arguments for/against.

Load Time

With SSR load times are shorter, since SSR sends finished HTML at the get-go, whereas CSR serves HTML -> Loads JS -> Fetches Data. This depends on the amount of data and files that need to be loaded on the page, but in drastic cases CSR will perform a lot worse than SSR.

Resources

SSR puts the load on the CPU to build the page, great for heavy pages, but can be worse if it's simple pages with high load. With CSR you would offload it to the user's browser.

SEO

For SEO (Search Engine Optimization), SSR will render the HTML page fully, whereas with CSR pages might not get crawled properly.

CORS

On CSR it can be quite the hassle to manage CORS, however, with SSR it won't matter, since both the API and the page rendering happens server side.

router.get('/', (req, res) => {
  res.send(frontpagePage);
})

router.get('/about', (req, res) => {
  res.send(aboutPage);
})

router.get('/contact', (req, res) => {
  res.send(contactPage);
})