Build your own global NPM module using Typescript
Tags: tutorial, npm, typescript
Published: 2021-05-29 14:50:23

Are you trying or interested in creating your own global NPM module? If you are, you've come to the right place! If not, I suggest you to stick around anyway.

A couple of weeks ago, I had this idea about making my own CLI tool for managing my AWS profiles instead of using the one that the [AWS CLI](https://aws.amazon.com/cli/) provides. I know, why reinvent the wheel? I didn't had the intention of releasing this tool, but I just wanted to know it would be done. And so I did.

In this tutorial, we're going to be creating a simple global NPM module that tells hilarious Chuck Norris jokes using [Chuck Norris Jokes API](https://api.chucknorris.io/). We're going include some cool stuff like async code and user prompts so you can see how everything works in a typical CLI tool.

### Setting up your project
For this tutorial, you're going to need to have Node installed on your computer. You can do so by going to [Node's official website](https://nodejs.org/) and following the instructions there.

After you're done with that, you're going to do the following:
1. Open up your terminal
2. Create a new directory called `chuck-me-a-joke`
3. Navigate to that directory
4. Run `npm init` and setting up your Node package (you can just leave everything as it is or modify whatever you want)

Cool! Now that you have created your project, we can move up to the next step which is installing everything you need.

### Packages installation
#### Typescript
As you might already figured out by the title, we're going to use Typescript for this project. If you're not already familiarized with Typescript, don't worry, you can take a look at [this post](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) that will get you started.

We're going to install Typescript as a dev dependency. We can  install it by running the following commands:
````bash
npm install typescript --save-dev
npm install @types/node --save-dev
```

Next, we're going to set up Typescript. First, let's run the following command:
````bash
npx tsc --init
```

This will generate a `tsconfig.json` file on the root of your project that will contain the configuration that Typescript will use for building our project. Open this file and replace it's content with the following:
```json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false,
    "outDir": "dist"
  },
  "exclude": [
    "node_modules"
  ]
}
```

Finally, we need to add our `build` script so that our Typescript project can be compiled to JavaScript. We can do so by adding this line to the `scripts` on our `package.json`:
```json
"build": "tsc"
```

#### Prompts
Now that we've got Typescript set up, we're going to install [Prompts](https://www.npmjs.com/package/prompts). This a really cool library for managing user interaction with the command line. You can write text, select from a list, add colors, and much more.

You can install it by running the following commands:
```bash
npm i prompts
npm i @types/prompts --save-dev
```

#### Axios
[Axios](https://www.npmjs.com/package/axios) is a pretty simple but powerful library that allows you to easily make http requests. We're going to use this for fetching jokes from the API. Run the following command for installing Axios:
```
npm i axios
```

### Folder structure
All right, we now have everything we need to start making our NPM module.

First, let's create our (simple) folder structure. For this, just create a new folder called `src` in the root of your project, and create a new file inside it called `index.ts` which will serve as the entry point for the application. This means that we need to specify this on our `package.json`. For this, just open the `package.json` file and replace this:
```json
"main": "index.js"
```
with this:
```json
"main": "dist/src/index.js"
```

But hold on, what's this `dist` folder doing there? This is the folder that we told Typescript to build our application in. We added this in the `tsconfig.json` file when we set up Typescript.

### Code time!
Ok! Let's code!

First, we're going to create a service that fetches jokes from the API. For this, we're going to create a folder called `services` inside our `src` folder. Then, we're going to create a `jokes.service.ts` file with the following content:
```typescript
import axios from 'axios';

const CHUCK_JOKES_URL = 'https://api.chucknorris.io/jokes/random';

type Joke = {
    value: string;
}

export async function getJoke(): Promise<Joke> {
    try {
        const response = await axios.get(CHUCK_JOKES_URL);
        return response.data;
    } catch (err) {
        throw new Error('Failed to fetch Chuck joke.');
    }
}
```

Neat! We've got ourselves a service that gets random jokes. Now we need to call it from our `index.ts` file.

Copy the following code into our `index.ts`:
```typescript
#!/usr/bin/env node

import { getJoke } from "./services/jokes.service";

(async function chuckJokes() {
    const joke = await getJoke();
    console.info(joke.value);
})();
```

The first line in our `index.ts` is called a [shebang line](https://en.wikipedia.org/wiki/Shebang_(Unix)). This is used by Unix based systems to determine what interpreter should run this file. Windows doesn't allow shebangs because they rely on the file extension, so it just ignores this line.

Then, we have an immediately-invoked async function called `chuck-me-a-joke` that fetches a random joke from the API and then it just prints it out. This can also be done using [top-level await](https://v8.dev/features/top-level-await) in newer Node versions.

Now, you can run `npm run build && node dist/src/index.ts` and it will print a random joke. Cool. But we want to just run a single command on our terminal. For this, we need to define the [`bin`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bin) property on our `package.json` which maps the name of the package (chuck-me-a-joke) to our executable file. Being that said, add the following line to our `package.json`:
```json
"bin": "dist/src/index.js"
```

Then, we can run `npm run build && npm i -g .` to install the package globally (remember to be placed on the root of the project).

Now, if you run `chuck-me-a-joke`... BOOM! We're prompted with a hilarious Chuck Norris joke on our terminal.

That looks cool, but you've probably seen some CLI tools that allow parameters to be passed to the function. Let's make our function accept the following two parameters: 
- `random` which gives the user a random Chuck Norris joke.
- `category` which gives you a list of categories to pick from and returns a Chuck Norris joke from that category.

First of all, let's modify our `getJokes` function in the `jokes.service.ts` so that it accepts an optional category parameter:
```typescript
import axios from 'axios';

const CHUCK_JOKES_URL = 'https://api.chucknorris.io/jokes/random';

type Joke = {
    value: string;
}

export async function getJoke(category?: string): Promise<Joke> {
    try {
        const response = await axios.get(category ? `${CHUCK_JOKES_URL}?category=${category}` : CHUCK_JOKES_URL);
        return response.data;
    } catch (err) {
        throw new Error('Failed to fetch Chuck joke.');
    }
}
```

Ok! Now, let's modify our `index.ts` to accept the parameters that we've just discussed to be passed in the command line:
```typescript
#!/usr/bin/env node

import prompts = require("prompts");
import { getJoke } from "./services/jokes.service";

const chuckJokesCategories = [
    { title: "Animal", value: "animal" },
    { title: "Career", value: "career" },
    { title: "Celebrity", value: "celebrity" },
    { title: "Developer", value: "dev" },
    { title: "Explicit", value: "explicit" },
    { title: "Fashion", value: "fashion" },
    { title: "Food", value: "food" },
    { title: "History", value: "history" },
    { title: "Money", value: "money" },
    { title: "Movie", value: "movie" },
    { title: "Music", value: "music" },
    { title: "Political", value: "political" },
    { title: "Religion", value: "religion" },
    { title: "Science", value: "science" },
    { title: "Sport", value: "sport" },
    { title: "Travel", value: "travel" }
];

(async function chuckJokes() {
    const args = process.argv.splice(2);
    const arg = args[0];

    if (args.length > 1) {
        return console.info("You can only pass one argument; `random` or `category`");
    }

    if (!arg) {
        return console.info("You need to pass one of the following arguments: `random` or `category`.");
    }

    let joke;
    if (arg === 'random') {
        joke = await getJoke();
    } else if (arg === 'category') {
        const category = await prompts({
            type: 'select',
            name: 'value',
            message: 'Pick a category',
            choices: chuckJokesCategories,
            initial: 1
        });
        joke = await getJoke(category.value);
    } else {
        return console.log(`Sorry, ${arg} is not a valid argument.`);
    }

    console.info(joke.value);
})();
```

[process.argv](https://nodejs.org/en/knowledge/command-line/how-to-parse-command-line-arguments/) is an array that contains the list of parameters passed in the command line. We're removing the first two parameters that are `node` and the path to the script because we don't really care about them.

Then, if `chuck-me-a-joke` is called without any params or with more than one, we'll get an error saying that we need to pass the `random` or `category` params.

Finally, if the parameter is `random`, we'll return a random Chuck Norris joke. If the parameter is `category`, we will ask the user to select one of the categories using the Prompts library and we'll show a joke that belongs to the chosen category.

Remember that you need to rebuild and reinstall the module every time you make changes. Otherwise `chuck-me-a-joke` will not be updated. You can do this by running `npm run build && npm i -g .`

### Publishing
Before publishing our module, we need to add a README.md file to the root of the project containing information about what it does, how it works and all of that stuff. I won't add it here because I don't think is necessary but feel free to look at other npm packages to have an idea of what this README file should have.

Now, before publishing it, we need to create an account in [NPM](https://www.npmjs.com/signup). After signing up and confirming our email, go to your terminal and in your project root run `npm adduser`.

This will ask for your username and password to validate that is really you who is trying to publish the package.

After that just run `npm publish --access public` and that's it! We have our own global NPM module published and for the people to use and enjoy.

### Outro
I hope this post was somewhat useful for you.

As with every code, you should probably add tests to it. I didn't do it on this post because I believe it's out of scope, but [here's a link to the GitHub repository](https://github.com/augusto1024/chuck-me-a-joke) that contains the final version which includes tests.

You can check the module in [NPM](https://www.npmjs.com/package/chuck-me-a-joke) as well.

Special thanks to [Mathias](https://github.com/matchilling) for letting me use his awesome [Chuck Norris Jokes API](https://api.chucknorris.io/)!

Also, special thanks to Chuck Norris for existing. This wouldn't be possible without you.

See you in the next one!