DEV Community

Cover image for Create a pure Node.js and TypeScript app + Getting node-fetch ESM-only (version 3) up and running
aderchox
aderchox

Posted on • Edited on

Create a pure Node.js and TypeScript app + Getting node-fetch ESM-only (version 3) up and running

Introduction

Node.js is a browserless (server-only) runtime environment for JavaScript apps, node-fetch gives Node.js apps an API similar to the browsers' standard fetch() API, and TypeScript provides static typing which is cool. In this article, we're going to bootstrap a project that glues these three together.

node-fetch version 3 is ESM-only

Node.js apps used a module system called CommonJS in the past (modules in this system are imported like this: const x = require("/path/to/module");). ESM system wasn't a thing yet then, but since its emerge (first in browser environments), Node.js has also started the transition (which has some benefits compared to CommonJS system, and its imports are like this: import x from "/x.js").

Since the release of Node.js version 14 in April 2020, it has had stable support for the ESM module system. So no wonders package authors have also tried their best to keep up to speed with this transition and have provided ESM support for their packages too.

Usually, package authors support both these module systems (despite the difficulties of doing it, e.g., automating, maintaining, bugs, etc.) for the sake of backwards compatibility, however, among all packages, there are also ones whose authors have pulled the future close to "now" faster..., and are offering "ESM only". fetch-node version 3 is an example of these ESM-only packages.

The Steps

  • 1. Create a directory for the project, cd into it, initiate a Node.js project, and install TypeScript and node-fetch packages:
mkdir ts-node-fetch-app
cd ts-node-fetch-app
npm init -y
npm i typescript node-fetch@3
Enter fullscreen mode Exit fullscreen mode
  • 2. Create app.ts:
import fetch from "node-fetch";

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

async function fetchTodo(id: number) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${id}`
  );
  const todo = await response.json();
  return todo as Promise<Todo>;
}

const todoId = 1;
fetchTodo(todoId).then((todo) => console.log(todo.title));
Enter fullscreen mode Exit fullscreen mode

NOTICE: In a real project, you'd commonly have a src directory for your source files and a build directory for your compiled modules and so on. This is a quick project however, for demonstration purposes only. So we've kept things utterly minimal and [hopefully] on-point.

  • 3. Generate a configuration file for TypeScript (tsconfig.json):
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Now we should have a tsconfig.json file with some default options in it. We need to change the value of the option module though, so that the module code generated by the TypeScript compiler will work on Node.js. We'll change its value from "commonjs" to "node16".

NOTICE: We could use "nodenext" too, but "node16" suffices for our purposes. You may also put the cursor in the middle of the double quotations and hit CTRL+SPACE to get intellisense on the available values for each of the options.

After making the above change and removing all the comments, this is our final tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "Node16",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true, 
    "strict": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 4. Since node-fetch version 3 is ESM-only, we have to ESM module system in all our Node.js project. Node.js, however, by default assumes all projects to be "commonjs", unless otherwise specified using in the package.json. So in the package.json add:
"type": "module"
Enter fullscreen mode Exit fullscreen mode
  • 5. Now compile the app.ts to JavaScript:
npx tsc
Enter fullscreen mode Exit fullscreen mode
  • 6. And run it:
node app.js // Prints: delectus aut autem
Enter fullscreen mode Exit fullscreen mode

Funny GIF titled Give Me More

Bonuses

  • 7. We can also use top-level await in our app.ts instead of the chaining .then() syntax used. Top-level 'await' expressions though are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher. Our tsconfig.json is fine for the most part, except for the target option which is on "es2016". Let's tweak it quickly and change it to "ES2022":
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true, 
    "strict": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Now app.ts may be re-written with top-level await in it:

import fetch from "node-fetch";

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

async function fetchTodo(id: number) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${id}`
  );
  const todo = await response.json();
  return todo as Promise<Todo>;
}

const todoId = 1;
// Top-level await:
const todo = await fetchTodo(todoId);
console.log(`Todo Title: ${todo.title}`);
Enter fullscreen mode Exit fullscreen mode

Now compile and run it with:

npx tsc
node app.js
Enter fullscreen mode Exit fullscreen mode
  • 8. Currently, every time we make a change, we have to manually re-compile and re-execute our project. This is not cool. So let's use a tool that can do these both automatically for us. I'll use tsx. Check out their repository and also don't forget to leave them a star if you find it useful.

NOTICE: Obviously you can use any other packages too, but make sure to check their ESM support beforehand. For instance, at the time of writing this article, ts-node-dev's ESM support seems to be missing support ESM yet.

npm i -D tsx
Enter fullscreen mode Exit fullscreen mode

NOTICE: We'll use it in development only, so I've added a -D option to specify it as a devDependency in the package.json and later on we'll be able to prevent its installation once not needed anymore, e.g., using npm install --omit=dev.

Now add this script in the package.json:

"dev": "tsx watch --tsconfig ./tsconfig.json ./app.ts"
Enter fullscreen mode Exit fullscreen mode

Now run the project with:

npm run dev
Enter fullscreen mode Exit fullscreen mode
  • 9. We may also use types similar to the types defined in microsoft/TypeScript for the standard fetch() API, and define a custom fetcher function that is typed generically.

NOTICE: An easy way to find these standard types is by typing them in the editor and ctrl-clicking on them.

We've called this generically typed function "fetchData". Here's the final app.ts code:

import fetch, { RequestInfo, RequestInit } from "node-fetch";

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

type Request = RequestInfo | URL;

async function fetchData<T>(request: Request, init?: RequestInit) {
  const response = await fetch(request, init);
  const todo = await response.json();
  return todo as Promise<T>;
}

async function fetchTodo(id: number) {
  return await fetchData<Todo>(
    `https://jsonplaceholder.typicode.com/todos/${id}`
  );
}

const todoId = 1;
const todo = await fetchTodo(todoId);
console.log(`Todo Title: ${todo.title}`);
Enter fullscreen mode Exit fullscreen mode

Hey, you're invited to join our small and fledging 🐥🐣 Discord community called TIL (stands for "Today-I-Learned") where we share things that we learn with each other, in the realm of web programming. Join using this link.

Thanks for reading this article 👋 You're also very welcome to suggest any fixes and improvements in the comments section below.

Top comments (0)