Susan Potter
software: Created / Updated

Cross-runtime TypeScript Library Development: A Deno-Node Workflow

Example TypeScript code to show how to import Node APIs and NPM packages via ES Modules (ESM)
Example TypeScript code to show how to import Node APIs and NPM packages via ES Modules (ESM)

Hey there, TypeScript developers! If you’re like me, you crave the secure and sandboxed environment that Deno provides. Deno also has fantastic out-of-the-box TypeScript, linting, formatting, testing, benchmarking support that makes development a breeze and frees you and your team from decision overload and multiple tool configuration paralysis. But what if you’re working on a library that needs to also support Node.js users? Fear not, because in this blog post, I’ll show you how to seamlessly maintain a TypeScript library using Deno as your primary toolchain while still ensuring compatibility with Node.js consumers.

Who Is This For?

This article is specifically designed for developers who build libraries for Node.js users but prefer the development experience offered by Deno. You might be wondering, “Can Deno handle my Node-isms?” Well, fear not! Deno has got your back. Head over to this page for more specifics on Deno’s compatibility with Node.js.

Before We Dive In (Prerequisites)

First, let’s make sure you have the necessary tools available in our environment. Here’s what I’ll use:

In the next section I will talk about setting this up using the Nix package manager . If you aren’t interested in that you can skip and set the above up in your choice of package manager.

Nix Setup (optional)

If you’re eager to get started right away and want to set up a quick ephemeral development environment using the Nix shell, try this command:

nix-shell -I nixpkgs=channel:nixos-23.05 -p nodejs-18_x deno esbuild

The versions you’ll get from this command (or similar) should be something like:

[nix-shell]$ node --version
v18.16.0

[nix-shell]$ deno --version
deno 1.33.3 (release, x86_64-unknown-linux-gnu)
v8 11.4.183.2
typescript 5.0.4

[nix-shell]$ esbuild --version
0.17.19

These are the versions I used for our uber simple demo, but feel free to adapt to your preferred setup.

If you want a Nix flake for your project after trying this out, try using the following flake.nix:

{
  description = "TypeScript library dev, Deno/Node workflow";

  nixConfig.bash-prompt = "\[ts-deno-node\]> ";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/release-23.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages."${system}";
        inherit (pkgs) mkShell;
      in
      {
        overlay = final: prev: {
        };

        devShell = pkgs.mkShell {
          buildInputs = with pkgs; [ deno nodejs-18_x esbuild ];
        };
      }
    );

}

Once you have the flake.nix in the root of your project repo and added to your git repository you can run nix develop in the root dir. If you have direnv installed add a .envrc with the contents use flake in it then direnv allow path/to/project/root/here. This will automatically always load the flake when you enter the project directory.

The purpose of the Nix flake it to make the core tool dependencies reproducibly setup each time you work in the library. When you bootstrap the Nix environment the firs time, Nix will create a lock file for your flake so until you update the lock file or Nix flake file, you will be using the same versions!

Setting Up Environment Variables

Let’s get your environment ready by setting the NPM_CONFIG_REGISTRY environment variable. This variable allows you to customize the URL for the npm registry. Just run the following command:

export NPM_CONFIG_REGISTRY=https://registry.npmjs.org

Let’s Build a Test Library

To illustrate our workflow, we need a small “library” that we can test and bundle for Node.js. Here’s a TypeScript library of exported validation functions to help us demonstrate the validity of the workflow on a small surface area:

import url from 'node:url';

export function isValidSlug(slug: string): boolean {
  return slug.length > 0;
}

export function isValidId(id: number): boolean {
  return id > 0;
}

export function isValidTitle(title: string): boolean {
  return title.length > 0;
}

export function isValidUrl(url: string): boolean {
  return url.parse(url).ok;
}

Don’t worry too much about the code logic here; it’s just a simple example to demonstrate the process. We have a few functions that perform basic validations that are exported using ES Modules.

Some quick notes:

Testing the Library on Node.js

Now that we have our library, it’s time to test it on Node.js. We’ll create a test suite using the built-in assert module. Here’s an example:

const assert = require("node:assert");
const lib = require("./lib");

assert.equal(true, lib.isValidId(1));
assert.equal(false, lib.isValidId(-1));
assert.equal(false, lib.isValidSlug(""));
assert.equal(true, lib.isValidSlug("hello"));

console.log("All library tests passed!");

We can save this code in a file called libtest.js. When we run it with Node.js, we should see the following output:

$ node libtest.js
All library tests passed!
Perfect! Our library is working as expected in the Node.js environment.

Bundling with Esbuild

Before we continue running tests on Node.js, we need to generate a bundled version of our library specifically for the Node.js platform. To achieve this, we’ll use esbuild , a fast and efficient bundler. Execute the following command:

esbuild --bundle ./lib.ts --platform=node > lib.js

Now, when we run the tests again, we’ll get the expected output:

$ node libtest.js
All library tests passed!

Fantastic! Our bundled library is fully compatible with Node.js.

Why Bother with Deno?

You might be wondering, “Why go through all this trouble when I can stick with Node.js for library development?” Well, my friend, there are several advantages to embracing Deno alongside Node.js:

Sandboxed Development Environment
Deno provides a secure sandboxed environment, which is excellent for local development. By delegating Node.js testing to your CI/CD pipeline, you can keep your development environment contained and avoid any unforeseen issues.
ES Modules Out of the Box
Deno supports ECMAScript Modules (ESM) at the source level, without any additional configuration. This means you can utilize modern module syntax without tinkering with package.json or webpack configurations.
Seamless NPM Package Imports
Deno makes importing NPM packages a breeze. You can simply use a syntax like this: import { option, either } from “npm:fp-ts@^2.16”; Deno takes care of handling the package resolution for you.
Effortless Version Management
Deno introduces import maps, which allow you to manage versions across multiple source files effortlessly. By creating an import map JSON file, such as imports.json, in your project/package root directory and adding an key-value entry like "fp-ts": "npm:fp-ts@^2.16" to the imports map, you can ensure consistent versioning. When running your program, you can refer to the import map file using the --import-map ./import.json option or embed your import map in the project’s deno.json and simplify with good defaults.
Enhanced Developer Ergonomics
Deno offers a superior developer experience compared to Node.js. It provides TypeScript support out of the box, eliminating the need for extensive tsconfig.json configurations. Deno’s LSP (Language Server Protocol) enables powerful code editing features, and you can format and check your codebase using Deno without relying on additional tools. Deno even includes a benchmarking and testing library in its stdlib, saving you from adding extra dependencies. Deno’s default settings cover most scenarios, and if you need to override them, simply create a deno.jsonc file with the desired settings. Plus, it supports comments! Some key fmt config options I override are lineWidth (120, hey we have bigger screens now than the 1970s), singleQuote (true). Consult the configuration reference docs .
Cross-Platform Binary Building
Deno allows you to build cross-platform command-line tools directly from your codebase using the deno compile command. Unlike Node.js, where this process can be quite complex, Deno simplifies it, saving you time and effort.
Testing Fixes on Node.js
Occasionally, you may need to test a fix specifically on Node.js. With esbuild’s watch capabilities, you can easily iterate and test changes in a tight loop on the Node.js platform.

With all these benefits, Deno empowers you to make informed decisions, reduces unnecessary extran dependencies or additional configuration maintenance, and streamlines your project setup.

Conclusion

Cheers for making it to the end of the blog post, I am hoping you found it valuable! You’ve learned how to leverage Deno’s power while ensuring compatibility with Node.js. By embracing Deno’s sandboxed environment, seamless NPM package imports, and enhanced developer ergonomics, you can build libraries that cater to both Deno and Node.js users. So go ahead, start your deno.json (or better yet deno.jsonc) file (remember you can just rename imports.json), and enjoy the freedom of a streamlined and forward-looking development workflow without the overhead of decision overload at each step of the way. Say goodbye to unnecessary decisions, tooling and twenty different configuration files at your project root directory, and let Deno do the heavy lifting for you. Happy coding!

If you enjoyed this content, please consider sharing this link with a friend, following my GitHub, Twitter/X or LinkedIn accounts, or subscribing to my RSS feed.