DEV Community 👩‍💻👨‍💻

Cover image for Build your own global NPM module using Typescript
Augusto
Augusto

Posted on

Build your own global NPM module using Typescript

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 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. 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 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 that will get you started.

We're going to install Typescript as a dev dependency. We can install it by running the following commands:

npm install typescript --save-dev
npm install @types/node --save-dev
Enter fullscreen mode Exit fullscreen mode

Next, we're going to set up Typescript. First, let's run the following command:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

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:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false,
    "outDir": "dist"
  },
  "exclude": [
    "node_modules"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

"build": "tsc"
Enter fullscreen mode Exit fullscreen mode

Prompts

Now that we've got Typescript set up, we're going to install 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:

npm i prompts
npm i @types/prompts --save-dev
Enter fullscreen mode Exit fullscreen mode

Axios

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
Enter fullscreen mode Exit fullscreen mode

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:

"main": "index.js"
Enter fullscreen mode Exit fullscreen mode

with this:

"main": "dist/src/index.js"
Enter fullscreen mode Exit fullscreen mode

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:

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.');
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

#!/usr/bin/env node

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

(async function chuckJokes() {
    const joke = await getJoke();
    console.info(joke.value);
})();
Enter fullscreen mode Exit fullscreen mode

The first line in our index.ts is called a shebang line. 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 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 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:

"bin": "dist/src/index.js"
Enter fullscreen mode Exit fullscreen mode

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:

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.');
    }
}
Enter fullscreen mode Exit fullscreen mode

Ok! Now, let's modify our index.ts to accept the parameters that we've just discussed to be passed in the command line:

#!/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);
})();
Enter fullscreen mode Exit fullscreen mode

process.argv 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. 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 that contains the final version which includes tests.

You can check the module in NPM as well.

Special thanks to Mathias for letting me use his awesome Chuck Norris Jokes API!

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

See you in the next one!

Top comments (0)

🌚 Life is too short to browse without dark mode