Module Systems in Node.js

Enabling ES Modules

Up until now, we have used CommonJS for writing scripts and imports, meaning that if we wanted to share code between files, they would have to all be imported as scripts in the html files. However, if we add "type": "module" to our package.json, it enables ES Modules syntax in Node.js.

{
  "type": "module",
  "dependencies": {
    "express": "5.2.1"
    }
}

Now we can export from one file to anothers and share code between the two without having to use a html file between them.

CommonJS

CommonJS Exporting

In common JS we use require() and module.exports to export. This is no longer the recommended approach, but it was default for Node.js for many years

Exporting:

const cookie = require('./cookie.json');

function getCookie() {
  return cookie;
}

module.exports = { getCookie };

CommonJS Importing

Importing:

const cookiesUtil =
  require('./util/commonjsCookiesUtil.js');

This demonstration of CommonJS was purely intended as a educational purpose. Going forward, we will use ES Modules

ES Modules

ES Modules Exporting

ES Module use export and import key words, which are also supported natively in browsers, that means we can use the same syntax on the client and the server. This is already a way cleaner approach, since we can use the same syntax all across our codebase.

Exporting:

// Named export, can have multiple per file
export function esModuleCookieFactory() {
  return 'On a break';
}

// Default export, only one per file
export default {
  esModuleCookieFactory
};

ES Modules Importing

Important rule: Always keep the .js extenstion in ES Module import paths, Node.js does not resolve the extension automatically.

Importing:

// Named import (curly braces,
// can define more than one)
import { esModuleCookieFactory }
  from './util/esModuleCookiesUtil.js';

// Default import (no curly braces,
// for single imports)
import cookiesUtil
  from './util/esModuleCookiesUtil.js';

Always use the .js extension in ES Module imports.

Named vs Default Exports

Like we touched upon earlier, it is not possible to have more than one default exports per script, but we can have many named exports.

Named exports are generally preferred, since you always know exactly what you are importing.

Named exports (recommended):

// Export multiple
export function func1() { }
export const value = 42;

// Import specific
import { func1, value }
  from './module.js';

Default export:

// One per file
export default function() { }

// Import without braces
import myFunction from './module.js';

Client-Side Module Loading

Traditional Script Loading

Regular scripts share global scope with every other script in the page, meaning variables can be passed from one file to another and accidentally overwrite another. Load order also needs to be managed manually.

<!-- Scripts share global scope -->
<script src="./common.js"></script>
<script src="./cookieFactory.js"></script>

Module Script Loading

With type="module", every script has its own scope and can share code. The browser handles load order automatically:

<!-- ES Module - isolated scope -->
<script type="module"
  src="./cookies.js">
</script>

Static File Management

The Problem

When the browser renders a page, it will also request the affiliated CSS, JavaScript and image files.

But without explicit permission from the server, the requests will always return a 404, so we need to tell Express which folder it should serve these files from.

Express Static Middleware

express.static() is a built-in Express middleware that serves any file inside the specified folder automatically. In this case we named it 'public' and that becomes directly accessible from the browser.

import express from 'express';
const app = express();

// Serve files from 'public' directory
app.use(express.static('public'));

Serving HTML Pages

For specific page routes, we use res.sendFile(). This requires an absolute path, which is not very useful. We can then use path.resolve() to construct a relative path instead.

Path Resolution in ES Modules

In the earlier weeks, we used __dirname which is a CommonJS feature and will not work in ES modules projects. path.resolve() is the replacement.

import path from 'path';

app.get('/', (req, res) => {
  res.sendFile(path.resolve(
    'public/frontpage/frontpage.html'
  ));
});

app.get('/cookieFactory', (req, res) => {
  res.sendFile(path.resolve(
    'public/cookieFactory/cookieFactory.html'
  ));
});

Project Organization

Client vs Server Separation

When working on full stack projects, clean separation between the client and server code makes the project easier to maintain and navigate. The public/ folder belongs to the browser, everything else belongs to the server.

project/
├── public/           # client
   ├── frontpage/
   ├── frontpage.html
   └── frontpage.css
   └── cookieFactory/
├── util/             # server
   └── esModuleCookiesUtil.js
├── package.json      # server
└── app.js            # server

HTTP Redirection Techniques

When to Use Redirects

We can redirect the user from the browser from one page to another. A common usecase for this, is redirecting after a form submission. For example, sending the user to a success page after a short delay.

Client-Side Redirection

JavaScript Timed Redirect

Timed redirect with JavaScript:

setTimeout(() => {
  window.location.href = '/success';
}, 2000);

HTML Meta Redirection

Or with a meta tag:

<meta http-equiv="refresh"content="3;url=/success">

Server-Side Redirection

When the redirect should happen on the server, for example to forward from an old URL to a new one, we use res.redirect(). The server sends back a 302 response, and the browser follows it automatically.

app.get('/old-page', (req, res) => {
  res.redirect('/new-page');
});