Create a pure Node.js and TypeScript app + Getting node-fetch ESM-only (version 3) up and running
Tags: typescript, node, tutorial, webdev
Published: 2023-03-16 19:22:30

### 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
```

- 2. Create `app.ts`:
```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));
```

> <sub>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.</sub>

- 3. Generate a configuration file for TypeScript (tsconfig.json):
```
npx tsc --init
```

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".

> <sub>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 <kbd>CTRL</kbd>+<kbd>SPACE</kbd> to get intellisense on the available values for each of the options.</sub>

After making the above change and removing all the comments, this is our final `tsconfig.json`:
```json
{
  "compilerOptions": {
    "target": "ES2016",
    "module": "Node16",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true, 
    "strict": true,
    "skipLibCheck": true
  }
}
```
- 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:
```json
"type": "module"
```

- 5. Now compile the `app.ts` to JavaScript:
```
npx tsc
```

- 6. And run it:
```
node app.js // Prints: delectus aut autem
```


![Funny GIF titled Give Me More](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8nulcb3fk7ivucjb4bp.gif)

### 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":
```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true, 
    "strict": true,
    "skipLibCheck": true
  }
}
```

Now `app.ts` may be re-written with top-level await in it:
```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;
// Top-level await:
const todo = await fetchTodo(todoId);
console.log(`Todo Title: ${todo.title}`);
```
Now compile and run it with:
```
npx tsc
node app.js
```

- 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](https://github.com/esbuild-kit/tsx) and also don't forget to leave them a star if you find it useful.

> <sub>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](https://github.com/wclr/ts-node-dev)'s ESM support seems to be [missing](https://github.com/wclr/ts-node-dev/issues/314#issuecomment-1445231219) support ESM yet.</sub>

```
npm i -D tsx
```

> <sub>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`.</sub>

Now add this script in the `package.json`:
```json
"dev": "tsx watch --tsconfig ./tsconfig.json ./app.ts"
```
Now run the project with:
```
npm run dev
```

- 9. We may also use types similar to the types defined in [`microsoft/TypeScript`](https://github.com/microsoft/TypeScript/blob/main/src/lib/webworker.generated.d.ts#L6648) for the standard `fetch()` API, and define a custom fetcher function that is typed generically.

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

We've called this generically typed function "fetchData". Here's the final `app.ts` code:
```ts
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}`);
```

---------------------------------------
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](https://discord.gg/XxzKSgdSAD).

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