Using TypeScript in Node.js projects

Published at:Published at:Updated at:

TypeScript is tremendously helpful while developing Node.js applications. Let’s see how to configure it for a seamless development experience.

Setting up TypeScript

First, we need to install TypeScript. We can do this by running the following command:

npm i -D typescript

Next, we need to create a tsconfig.json file in the root of our project. This file will contain the TypeScript configuration for our project. Here is an example of a tsconfig.json file that I picked from Total TypeScript and added a few more things (read the code and pay attention to the comments):

{
  "compilerOptions": {
    /* Base Options: */
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "verbatimModuleSyntax": true,

    /* Setting ~ as the alias for the src/ directory */
    "baseUrl": ".",
    "paths": {
      "~/*": ["src/*"]
    },

    /* Strictness */
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,

    /* If transpiling with TypeScript: */
    "module": "NodeNext",
    "outDir": "dist",
    "sourceMap": true,

    /* AND if you're building for a library: */
    "declaration": true,

    /* AND if you're building for a library in a monorepo: */
    "composite": true,
    "declarationMap": true,

    /* If NOT transpiling with TypeScript: */
    "module": "preserve",
    "noEmit": true,

    /* If your code runs in the DOM: */
    "lib": ["es2022", "dom", "dom.iterable"],

    /* If your code doesn't run in the DOM: */
    "lib": ["es2022"],
  },
  /* I'm considering all your code is in src/ */
  "include": ["src/**/*.ts"]
}

Setting up the build script

Next, we need to set up a build script that will compile our TypeScript code to JavaScript. First, install tsc-alias to handle the aliases we defined in the tsconfig.json file:

npm i -D tsc-alias

Then, you can add the build script by adding the following script to our package.json file:

{
  "scripts": {
    "build": "tsc && tsc-alias"
  }
}

Setting up the development script

Next, we need to set up a development script that will watch for changes in our TypeScript files and recompile them. Personally, I like to use tsx, as it provides a much faster development experience compared to the built-in TypeScript watcher or ts-node. First, install tsx:

npm i -D tsx

Then, you can add the dev script (in order to start the project in development mode) by adding the following script to your package.json file:

{
  "scripts": {
    "build": "tsc && tsc-alias",
    "dev": "node --import=tsx --watch ./src/index.ts"
  }
}

Yes, you won’t get typechecks while developing using tsx, but you can run npm run build for that or add a new typecheck scripts to your package.json, and run it whenever you want to check for type errors:

{
  "scripts": {
    "build": "tsc && tsc-alias",
    "dev": "node --import=tsx --watch ./src/index.ts",
    "typecheck": "tsc --noEmit"
  }
}

Should I commit node_modules directory to git?

Published at:Published at:Updated at:

TL; DR: No. Please add node_modules to your .gitignore file:

node_modules

But, why?

The node_modules directory is where your package manager (that can be npm, yarn or pnpm) will install all the project dependencies listed on your package.json. Regardless of the package manager you choose, a lockfile (package-lock.json, yarn.lock or pnpm-lock.yaml, respectelly) will be generated in the first time you install your project dependencies, describing the entire dependency tree. This way, every time you need to reinstall your project dependencies, you shall get the exact same files.

The lockfile should be commited to git, enabling the re-installation of the tree of dependencies in any other ambient, what makes unecessary to commit the node_modules directory to git (also, it cuts the size of your repository by a lot, as node_modules can consumes gigabytes of space).

Rendering JSX on the Server with Fastify

Published at:Published at:Updated at:

JSX is an excellent abstraction for building web interfaces. Introduced by Facebook and popularized by React, it’s an extension of JavaScript designed to abstract nested function calls. It’s expected that JSX code will be pre-processed (transpiled) into valid JavaScript before being executed in browsers or environments like Node.js.

Project Setup

First of all, let’s start our project and install the necessary dependencies:

npm init -y
npm i fastify react react-dom
npm i -D @types/node @types/react @types/react-dom tsx typescript

Now, we set up the scripts for our project. The package.json should look like this:

{
  "type":  "module",
  "name":  "fastify-react",
  "version":  "1.0.0",
  "author":  "Douglas Moura <douglas.ademoura@gmail.com>",
  "description":  "POC on rendering React components from Fastify",
  "main":  "dist/main.js",
  "scripts": {
    "start":  "tsc && node dist/main.js",
    "dev":  "tsx --watch src/main.tsx",
    "build":  "tsc"
  },
  "license":  "ISC",
  "dependencies": {
    "fastify":  "^4.25.2",
    "react":  "^18.2.0",
    "react-dom":  "^18.2.0"
  },
  "devDependencies": {
    "@types/node":  "^20.11.6",
    "@types/react":  "^18.2.48",
    "@types/react-dom":  "^18.2.18",
    "tsx":  "^4.7.0",
    "typescript":  "^5.3.3"
  }
}

And this is the tsconfig.json that we will use:

{
  "compilerOptions": {
    "target":  "ESNext",
    "module":  "ESNext",
    "lib": [
      "dom",
      "es6",
      "es2017",
      "esnext.asynciterable"
    ],
    "skipLibCheck":  true,
    "sourceMap":  false,
    "outDir":  "./dist",
    "moduleResolution":  "node",
    "removeComments":  true,
    "noImplicitAny":  true,
    "strictNullChecks":  true,
    "strictFunctionTypes":  true,
    "noImplicitThis":  true,
    "noUnusedLocals":  true,
    "noUnusedParameters":  true,
    "noImplicitReturns":  true,
    "noFallthroughCasesInSwitch":  true,
    "allowSyntheticDefaultImports":  true,
    "esModuleInterop":  true,
    "emitDecoratorMetadata":  true,
    "experimentalDecorators":  true,
    "resolveJsonModule":  true,
    "baseUrl":  ".",
    "jsx":  "react-jsx"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "./src/**/*.ts",
    "./src/**/*.tsx"
  ]
}

Creating our components

The React ecosystem already provides the necessary tools for rendering our components to HTML and sending them directly from the server to our client. So, first, let’s create the root component:

// src/components/root.tsx

type  RootProps  = {
  children:  React.ReactNode
  title:  string
}

export  function  Root({ children, title }:  RootProps) {
  return (
    <html  lang="en">
      <head>
        <meta  charSet="utf-8"  />
        <meta  name="viewport"  content="width=device-width, initial-scale=1"  />
        <title>{title}</title>
      </head>
      <body>{children}</body>
    </html>
  )
}

And our home page:

// src/components/index.tsx

export  function  App() {
  return (
    <h1>Hello, World!</h1>
  )
}

Configuring Fastify to Render Our React Component

As we don’t intend to load React to hydrate our HTML on the client side, we can use the renderToStaticMarkup function exported from react-dom/server. Our server initialization file will look like this:

import Fastify from  'fastify'
import { renderToStaticMarkup } from  'react-dom/server'

import { App } from  './components/index.js'
import { Root } from  './components/root.js'

type  RenderArgs  = {
  children:  React.ReactNode
  title:  string
}

const  render  = ({ title, children }:  RenderArgs) => {
  return `<!DOCTYPE html>${renderToStaticMarkup(
    <Root  title={title}>{children}</Root>
  )}`
}

const fastify =  Fastify({
  logger:  true,
})

fastify.get('/', async  function  handler(_request, reply) {
  reply.type('text/html')
  return  render({ children:  <App  />, title:  'Hello, World!' })
})

try {
  await fastify.listen({ port:  3000 })
} catch (err) {
  fastify.log.error(err)
  process.exit(1)
}

If you start the project now (npm run dev), you should see the page at http://localhost:3000. Of course, we can enhance our implementation by using the new streaming API, introduced in React 18 (which is the recommended method). To do that, we will make the following changes to our code:

import Fastify from  'fastify'
import { renderToStaticNodeStream } from  'react-dom/server'
import { Transform } from  'node:stream'

import { App } from  './components/index.js'
import { Root } from  './components/root.js'

type  RenderArgs  = {
  children:  React.ReactNode
  title:  string
}

const  render  = ({ title, children }:  RenderArgs) => {
  let  isFirstChunk  =  true
  const  prepend  =  new  Transform({
    transform(chunk, _encoding, callback) {
      if (isFirstChunk) {
        isFirstChunk  =  false
        this.push('<!DOCTYPE html>')
      }
      callback(null, chunk)
    },
  })

  return  renderToStaticNodeStream(
    <Root  title={title}>{children}</Root>
  ).pipe(prepend)
}

const  fastify  =  Fastify({
  logger:  true,
})

fastify.get('/', async  function  handler(_request, reply) {
  const  stream  =  render({ children:  <App  />, title:  'Hello, World!' })

  reply.type('text/html')
  reply.send(stream)
})

try {
  await  fastify.listen({ port:  3000 })
} catch (err) {
  fastify.log.error(err)
  process.exit(1)
}

And with that, we are able to render our React components on the server side and stream them to our client. Here is the link to the repository.

Generating MD5 hashes on Node.js

Published at:Published at:Updated at:

You can create hashes in Node.js without the need to install any external library. Usually, I create the following utility function in the projects I work on:

/**
 * Hashes a string using md5
 *
 * @param {string} str
 * @returns {string}
 */
export const md5 = (str) => createHash('md5').update(str).digest('hex')

And I use it to replace the md5 library whenever I come across it.

Note that you can create hashes for any algorithm supported by the OpenSSL version on your platform. On Linux and Mac, you can see which algorithms are available with the command openssl list -digest-algorithms.

Understanding Tail Call Optimization With JavaScript

Published at:Published at:Updated at:

Consider the following function that calculates the factorial of a number:

const factorial = (n) => {
  let result = 1;

  while (n > 1) {
    result *= n;
    n--;
  }

  return result;
};

Factorial

In Mathematics, the factorial of a non-negative integer (n!) is the product of all positive integers less than or equal to n.

The function above was implemented iteratively, that is, it uses a loop to calculate the factorial of a number. However, it is possible to implement the same function recursively (that is, a function that references itself):

const factorial = (n) => {
  if (n === 0) return 1;

  return n * factorial(n - 1);
};

The result of both functions is the same, however, the iterative function is much more efficient (in JavaScript) than the recursive function. In addition, if we try to calculate the factorial of a very large number, we encounter the error RangeError: Maximum call stack size exceeded. Let’s understand why this happens and how we can improve the recursive function.

Call Stack

A call stack is a data structure that stores information about a program’s functions. When a function is called, it is added to the execution stack, as well as all the functions it calls. When a function returns, it is removed from the execution stack. Each function added to the stack is called a stack frame.

In order to understand what is happening, let’s try to represent, graphically, how the calculation of the factorial of 6 is done with the iterative function:

factorial(6)result = 1while(6 > 1) { result *= 6 6--}result = 6while(5 > 1) { result *= 5 5--}result = 30while(4 > 1) { result *= 4 4--}result = 120while(3 > 1) { result *= 3 3--}result = 360while(2 > 1) { result *= 2 2--}result = 720

Now, compare it with the substitution model for calculating the factorial of 6 using the recursive function:

Note that, in the iterative function, the arrow shape is linear and we can see the state of each variable at each step. In addition, at each iteration of our loop, a calculation is performed and the variables stored in memory are updated. In the recursive function, the arrow shape is exponential and we cannot see the state of all variables in the first half of the processing. In addition, each time the function is executed, more memory needs to be used to store the resulting values of each execution.

But what does this mean? In order for JavaScript to calculate the factorial of 6 using the iterative function, the while condition is added to the stack, where its calculation is performed, the result variable is updated, and then the executed code block of the while is removed from the stack. This is done until the while condition is false, that is, until the value of n is less than or equal to 1.

In the recursive function, each call to the factorial function is added to the stack as many times as necessary until the if condition is false, that is, until the value of n is less than or equal to 1. This means that, to calculate the factorial of 6, the factorial function is added to the stack 6 times before being executed. And that’s why, when we try to calculate the factorial of a large number (100,000, for example), we encounter the error RangeError: Maximum call stack size exceeded: there is not enough space in the stack to store all the calls to the factorial function.

Introducing Tail Call Optimization

As defined by Dr. Axel Rauschmayer:

[…] whenever the last thing a function does is call another function, then this last function does not need to return to its caller. As a consequence, no information needs to be stored on the call stack and the function call is more like a goto (a jump). This type of call is called a tail call; not increasing the stack is called tail call optimization (TCO).

Now, we have discovered that our factorial calculation function is not tail recursive. But how can we make it tail recursive? With the help of another function:

const factorial = (n) => {
  return factorialHelper(n, 1);
};

const factorialHelper = (x, accumulator) => {
  if (x <= 1) {
    return accumulator;
  }

  return factorialHelper(x - 1, x * accumulator);
};

Now, our function is tail recursive: the last thing it does is call a function (and not calculate an expression, as in the first implementation). Now, let’s see the substitution model for calculating the factorial of 6 with our new factorial function:

factorial(6)factorialHelper(6, 1)factorialHelper(5, 6)factorialHelper(4, 30)factorialHelper(3, 120)factorialHelper(2, 360)factorialHelper(1, 720)720

The performance is superior to our first implementation, although it still doesn’t beat the performance of the iterative function. However, we still encounter the error RangeError: Maximum call stack size exceeded. But why does this happen? Because, despite our function being tail recursive, current versions of Node.js and browsers (with the exception of Safari) do not implement Tail Call Optimization (despite its inclusion in the EcmaScript specification since 2015).

But how will we solve this problem? With the help of another function, of course! For that, we will rely on the Trampoline pattern:

const trampoline = (fn) => {
  while (typeof fn === "function") {
    fn = fn();
  }

  return result;
};

Our trampoline function consists of a loop that invokes a function that wraps another function (what we call a thunk) until there are no more functions to execute. Let’s see how the implementation of our factorial function would look like with the Trampoline pattern:

const trampoline = (fn) => {
  while (typeof fn === "function") {
    fn = fn();
  }

  return fn;
};

const factorialHelper = (x, accumulator) => {
  if (x <= 1) {
    return accumulator;
  }

  // Now, a function returns another function
  return () => factorialHelper(x - 1, x * accumulator);
};

const factorial = (n) => {
  return trampoline(factorialHelper(n, 1));
};

And now, we can call our factorial function with a large number, without encountering the error RangeError: Maximum call stack size exceeded. Of course, depending on the factorial we want to calculate, we will encounter an Infinity, as it is a very large number (a number greater than Number.MAX_SAFE_INTEGER: 253 - 1). In this case, we can use BigInt:

const trampoline = (fn) => {
  while (typeof fn === "function") {
    fn = fn();
  }

  return fn;
};

const factorialHelper = (x, accumulator) => {
  if (x <= 1) {
    return accumulator;
  }

  return () => factorialHelper(x - 1n, x * accumulator);
};

const factorial = (n) => {
  // Converting values to BigInt
  //-------------------------------\/----------\/
  return trampoline(factorialHelper(BigInt(n), 1n));
};

Typing our function

And finally, let’s add the necessary types to our factorial function:

type Thunk = bigint | (() => Thunk);

const trampoline = (fn: Thunk) => {
  while (typeof fn === "function") {
    fn = fn();
  }

  return fn;
};

const factorialHelper = (x: bigint, accumulator: bigint): Thunk => {
  if (x <= 1) {
    return accumulator;
  }

  return () => factorialHelper(x - 1n, x * accumulator);
};

const factorial = (n: number) => {
  return trampoline(factorialHelper(BigInt(n), 1n));
};

References