Skip to main content

Command Palette

Search for a command to run...

Blazingly fast CLI with Bun 🚀

Updated
•9 min read
Blazingly fast CLI with Bun 🚀
B

Developer who is betting his career on React and TypeScript :)

Bun is freshly out of the oven (Seriously, the parent company’s name is Oven!) and it is a new JavaScript runtime and not a framework! It is really promising and most importantly Blazingly fast! I tried it out and it is fantastic in my opinion. So In this article, we will look into how to create a simple note-taking CLI using bun. The idea here is to give a glimpse of the core utils that Bun comes with, the package management, and the most hyped-up factor which is the speed.

What’s up with Node.JS?

So Node.JS came in and revolutionized the world of JavaScript. All the frameworks’ existence is because of Node.JS but as days passed, there were a lot of problems as well with it. The major one that I could think of is NPM and Node modules. In a complex codebase, the npm i script will take an eternity to install all the packages. Also, NodeJS is built on top of the V8 engine developed by Google. Now I don’t know the metrics to calculate the performance bottlenecks but Bun came out and crushed NodeJS concerning performance. Bun is written in the Zig programming language and written on top of the JavaScript core engine developed by Apple. This engine is known for its performance but it is a little bit hard to work with.

Advantages of Bun:

  1. Bun is a drop-in replacement for node. Most of the node’s core utils work in a bun except utils that are close to the V8 engine.

  2. Bun offers a great developer experience by being a toolkit for JavaScript including a test runner, debugger, bundler and package manager.

  3. Last but not the least, Speed. Right from executing a script to installing a package, Bun is very fast.

Features that Bun offers

  1. Supports TypeScript out of the box, no need to configure ts-node or any other things like that anymore.

  2. Supports ES Modules and CommonJS are both in the same file, no need to create mts or cts.

  3. Comes with a Test runner, and debugger to make things simple.

  4. Support for JSX.

  5. Environment variables are loaded without any need for the dotenv package.

  6. It has a built-in HTTP server, Web socket, SQLite database, and so on.

Should you dump Node.JS?

Although bun seems promising, It is too soon to say. Bun 1.0 is already out and people have started adopting it. But Node.JS is not going anywhere. Similar to this situation, earlier another runtime came out called Deno, by the same person who created Node and deeply regretted creating it. Like the meme, even though NodeJS revolutionized the world, The programmer who created it would never be satisfied with his code. Till now Deno has never been replaced or not even in a competing stage with node. It co-exists, and it has a good ecosystem. It is healthy to have Bun as well in the race. The JavaScript ecosystem is so advanced that after the framework race, we are now in the runtime race.

Let’s build a CLI!

Now, Let’s create a cutting-edge CLI app that can take notes for you and we will write this in Bun and explore the features that Bun is offering.

In this CLI app, we will cover the following.

  • The SQLite package that Bun ships in with.

  • Installing NPM packages.

  • Creating a CLI and processing the arguments and much more.

Even if you don’t want to follow along, just read the article and I promise you will learn something new. Before starting, you can find the finished project in this GitHub repository.


Step 0 - Creating the project:

To kick things start, create a new folder, navigate into it, and run the following command.

mkdir bun-cli && cd bun-cli
bun init -y

This is similar to npm init but here, bun scaffolds are a basic application, setting up a great place to start our journey. It will generate a few files which would look familiar.

Bun, as I said is a drop-in replacement for Node.JS so It supports node_modules. The bun.lockb file is similar to the package-lock.json or similar files for the other package manager. Again, bun is a package manager in itself so no external package managers are required! The index.ts file is the entry point of this application though you can change that in the package.json. If you execute the index.ts file using bun index.ts command, you can see the output printed in the terminal without any build step for typescript. As I said, It just works!


Step 1 - Registering our CLI

Since we are building a CLI application, We have to assign a name for our CLI and register it so that instead of running bun index.ts every time, we can run the CLI instead.

To do this, first, we need to name our CLI, for this demo, I will call it note. (Yes I have patented the name!). Now open your package.json and add this piece of code.

"bin": {
    "note": "./index.ts"
 },

“bin” means binary and here we are specifying a CLI name as the key and the subsequent file to be executed whenever this command is called.

Next, you have to add this line as the first line in the index.ts file.

#! /usr/bin/env bun

This line is called a Shebang! This line tells the shell to use bun as the runtime when the CLI is invoked in the terminal

Next, you have to execute 2 commands

bun link
bun link <project_name>

The initial command will register your Bun project in the global scope. The second command will install the CLI package into the global bun binaries so that It can be used anywhere in the command line. The project name can be taken from the name property of the package.json file.

To test this, we can run our command note and see the result.


Step 2 - Installing the packages

Create a src folder, this is where all of our source code will reside. After that run the following command to install a few dependencies.

bun add yargs
bun add -D @types/yargs

yargs is a library that will let you build great CLI apps. It will automatically do CLI parsing, help documentation, etc.


Step 3 - Setting up the “yargs” library

Now create a file called command.ts and paste the following code.

import yargs from "yargs"
import { hideBin } from "yargs/helpers"

yargs(hideBin(process.argv))
  .command(
    "new <note>",
    "Creates a new Note",
    (yargs) => yargs.positional("note", {
      description: "The content of the note",
      type: "string",
    }),
    (argv) => console.log(argv.note)
  )
    .parse()
đź’ˇ
I’ll call this the command or controller layer.

Here, we are declaring a command called “new” that takes in an argument and prints it out. Later we will be persisting this info in a database.

Now import this file into the main index.ts file.

#! /usr/bin/env bun
import "./src/command.ts";

Now run the command note new "walk the dog" in the terminal (you can give any note you want in quotes) and you can see it getting printed.


Step 4 - Setting up the database:

Let’s persist this value in a database. Bun comes in with a built-in SQLite database which is again “Blazingly fast”. To implement this, let’s create a new file called db.ts and paste the following code.

import { Database } from "bun:sqlite"

const db = new Database("../db.sqlite")
db.exec("PRAGMA journal_mode = WAL;")

export const note_table_query = db.prepare(`CREATE TABLE IF NOT EXISTS note (
  note_id INTEGER PRIMARY KEY AUTOINCREMENT,
  note TEXT NOT NULL
)`)

export function create_new_note(note: string): void {
  const query = db.query(`INSERT INTO note (note) VALUES (?)`);
  query.run(note);
}
đź’ˇ
I’ll call this the Repository layer as it is interacting with the database.

Also, create a file called db.sqlite file in the root of your project. This file will be your app’s database.

So in this file, you can see a bunch of things going on, Let me explain.

To interact with an SQL database, first, the database table should be created, So the query for that is created which will be executed in the index.ts file. Then the create_new_note function is responsible for the insertion of a new row into the table. Bun gives a great API to interact with this database. You can create a query and execute it whenever you want. For Example, in the create_new_note function, The query is first created and then executed. I like this approach.

Next import the note_table_query method in the index.ts file and execute the query.

#! /usr/bin/env bun
import "./src/command.ts";
import { note_table_query } from './src/db';

note_table_query.run();

As you can see here, The bun’s SQLite package first creates the Query and then exposes functions to execute the query. Here we have a run method that runs the query. There are methods like all that are used with a SELECT query that will return all the rows that are returned. Check out the documentation for more information.


Step 5 - Connecting the controller with the repository layer:

Now let’s persist the information that is received from the terminal into the database. I will create an intermediate layer that will do the business logic because if the code is written in the command.ts file, it will look ugly as yargs primarily takes in a lot of callbacks.

đź’ˇ
I will call this layer the Service layer

Create a file called handler.ts inside the src directory and paste the following code.

import { create_new_note } from "./db";

export function create_note_handler(note: string) {
    try {
        create_new_note(note);
        console.log(`${note} added successfully`);
    } catch (err) {
        console.error(err);
        console.error("Oops, Error!");
    }
}

So in the above code, we are importing the create_new_note function from the repository layer and passing the string that we received as an argument from the user. We are also doing a bit of error handling here.

So now we can call this function in our service layer in the controller layer (i.e. command.ts) as shown below.

import yargs from "yargs";
import { hideBin } from "yargs/helpers";

const { create_note_handler } = require("./handler");

yargs(hideBin(process.argv))
    .command(
        "new <note>",
        "Creates a new Note",
        (yargs) =>
            yargs.positional("note", {
                description: "The content of the note",
                type: "string",
            }),
        (argv) => create_note_handler(argv.note as string),
    )
    .parse();

As you can see, in the callback, I am calling the function we created in the service layer. Also, note that I am importing the create_note_handler method using the CommonJS syntax and it will work out of the box.

So now when we execute this app, we can store the data in the database.

With all these steps you have created your Command Line Interface in Bun. You can check the repository where I have implemented other CRUD functionalities. You can extend and make this application extensive. With all of these covered, we are at the end of this article.

Conclusion

The JavaScript ecosystem is fast evolving and we are seeing new libraries, frameworks and now even runtimes getting released. It could be overwhelming but at the same time quite interesting. Bun is a promising tool and It is worth checking out. I have used bun in some of my React and Svelte projects and I haven't had any complaints yet, So I'd recommend you also to use it. Try to convince your boss why your project should also migrate to Bun for no reason and get a 10x raise in your next appraisal! (Sarcasm!)

C

For me 'npm link' worked, instead of 'bun link', to install the cli.

Though the entire project was done with bun and no trace of node or npm was there in my project, 'npm link' worked. Even my index.ts file had this - '#! /usr/bin/env bun' in the fist line.

A
Alina1y ago

nice post

1
A
Alina1y ago

hello

L

I recently wanted to start a small food delivery business, but I didn't know where to start. I needed information on how to create a food delivery app, because it's very relevant now. I came across an article on a website that described food delivery app development in detail. There are a lot of useful tips and even examples that can be applied in practice. I especially liked that they talked about various bonuses and offers that can facilitate the creation of an app. The site helped me to understand that the process is not as complicated as it seemed at first and gave me confidence in my abilities.

1
L

I recently wanted to start a small food delivery business, but I didn't know where to start. I needed information on how to create a food delivery app, because it's very relevant now. I came across an article on a website that described food delivery app development in detail. There are a lot of useful tips and even examples that can be applied in practice. I especially liked that they talked about various bonuses and offers that can facilitate the creation of an app. The site helped me to understand that the process is not as complicated as it seemed at first and gave me confidence in my abilities.

N

The cli setup worked on my mac. On windows I could not get it to be registered globally. Just got command not found. ChatGPT plus myself could not get it to work. :(

2
R

Thanks for the article. Liked the style. Unfortuantly I got stuck, when I try and run note I get command not found returned by zsh

2