I am currently developing an isomorphic web application. You can see it in action e.g. https://erleben.enns.at/. A few days ago it took me some time to figure out how to be able to use the fetch API both on the client and the server.

Isomorphic JavaScript, also known as Universal JavaScript, describes JavaScript applications which run both on the client and the server. Why would you want such a thing? Well it’s a common approach if you want your single page application (SPA) to be also renderable by the server. So on the client the JavaScript is interpreted e.g. by Chrome or Firefox, whereas on the server it is interpreted typically by Node.js.

When you can render the HTML of your SPA on the server and send it right away to the client when it requets a page you gain certain advantages:

  • Better search engine visibility (though Google Bot is able to crawl and index single page applications).
  • Faster inital load times when the client requests the application for the first time. The client can dislpay the pre-rendered HTML right away. It does not have to execute some JavaScript first to load e.g. data from the backend.

We use the plain fetch API to load data from our backend. All modern browsers support this API. And for older browser polyfills such as whatwg-fetch exist.

But on Node.js there exists no such thing as the fetch API. To be able to use fetch inside Node.js you have to bundle a library like node-fetch with your server bundle. It took me some time to figure out how to get this to work in the context of our setup: Webpack (together with the webpack-node-externals plugin) and Typescript.

Webpack config

These are the relevant parts of our webpack config:

var webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');

const configs = (env, options) => {

  return ([
    {
      entry: {
        browser: [
          // misc polyfills
          'core-js/es/promise',
          'core-js/es/string',
          'core-js/es/typed-array',
          'whatwg-fetch',
          // react specifc polyfills. see
          // https://reactjs.org/docs/javascript-environment-requirements.html
          'core-js/es/array',
          'core-js/es/map',
          'core-js/es/set',
          'raf/polyfill',
          // the actual webapp
          './src/client.tsx',
        ]
      },

      output: {
        path: __dirname + '/dist',
        filename: '[name].js',
      },

            // Add the loader for .ts files.
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            loader: 'ts-loader',
            options: { logLevel: 'info', transpileOnly: true }
          },
        ],
      },
    },
    {

      entry: {
        server: './src/server.tsx',
      },

      output: {
        path: __dirname + '/dist',
        filename: '[name].js',
      },

      // Add the loader for .ts files.
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            loader: 'ts-loader',
            options: { logLevel: 'info', transpileOnly: true }
          },
        ],
      },
      target: 'node',
      externals: [nodeExternals({
        whitelist: ['node-fetch']
      })],
    }
  ]);
};

module.exports = configs;

Things to note:

  1. In the client bundle we include a fetch polyfill for older browsers (whatwg-fetch).
  2. We tell the webpack-node-externals plugin not to exclude the node-fetch package from the server bundle.

Server initialization

When initializing the server you have to make the fetch function globally available as outlined below:

<...>

// enable node-fetch polyfill for Node.js
import fetch from "node-fetch";
// tslint:disable-next-line:no-any
declare var global: any;
global.fetch = fetch;

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

export const initServer = (title: string, pathToFavicon: string) => {
  console.log(`Server is booting (cwd is ${process.cwd()})...`);

  <...>

  }).listen(PORT, () => {
    console.log(`Server is now listening on ${PORT}... (${new Date()})`);
  });
};

<...>

Fetch example

Finally an example of how we fetch data from our backend services. The fetch API can now be used both on the client and inside a Node.js environment.


// NOTE: You do not have to import
// or require anything to be able
// to use fetch here!

export async function fetchLocationInfo(locationId: string, proto?: string, host?: string): Promise<LocationDTO> {

  const url = `<...>`;
  const response = await fetch(url);

  if (!response.ok) {
    const httpErrorRootCause = new HttpErrorRootCause(response.status, response.statusText, await response.text());
    throw new HttpStatusException(httpErrorRootCause);
  }

  const json = await response.json();
  const obj = json as LocationDTO;

  return obj;

}

A note about Netcup (advertisement)

Netcup is a German hosting company. Netcup offers inexpensive, yet powerfull web hosting packages, KVM-based root servers or dedicated servers for example. Using a coupon code from my Netcup coupon code web app you can even save more money (6$ on your first purchase, 30% off any KVM-based root server, ...).