🌐 Translation: Translated from Korean.
How to Build Node.js CLI Tools from Scratch: Complete Guide with Commander.js & TypeScript
Are you tired of repetitive tasks slowing down your development workflow? Need a custom tool you can share with your team? Node.js CLI (Command Line Interface) tools are powerful assets that can dramatically boost developer productivity. In this comprehensive guide, we’ll walk through building professional CLI tools with Node.js and TypeScript, step by step. The best part? Node.js CLI development is approachable for beginners and immediately applicable in real-world scenarios.
Why Build Your Own CLI Tools?
Many developers spend significant time on repetitive tasks—creating files, applying code templates, running deployment scripts, and more. These operations can all be automated with custom CLI tools. Node.js is an ideal platform for CLI development thanks to its rich ecosystem and cross-platform support.
What you’ll learn:
-
Fundamental architecture of Node.js CLI tools
-
Command parsing with Commander.js
-
Type safety with TypeScript
-
Complete, production-ready examples you can use immediately
Core Concepts of CLI Development
What is a Node.js CLI Tool?
A Node.js CLI tool is a terminal-based program that accepts commands and options from users to perform specific tasks. Popular tools like npm, git, and docker are all CLI applications. With TypeScript, you can build safer, more maintainable CLI tools.
Why Choose Commander.js?
Commander.js is one of the most popular libraries for Node.js CLI development. Key advantages include:
-
Clean API: Intuitive and easy-to-learn interface
-
Auto-generated help: Built-in
--helpoption support -
Type safety: Seamless TypeScript integration
-
Active community: Millions of weekly npm downloads
The Case for TypeScript
Using TypeScript provides several benefits:
-
Catch type errors early during development
-
Boost productivity with IDE autocomplete
-
Safe refactoring with confidence
-
Clear interfaces for team collaboration
Hands-On: Building a File Management CLI Tool
Let’s build a practical file management CLI tool step by step.
Step 1: Project Setup
Create a new project and install required packages:
mkdir my-cli-tool
cd my-cli-tool
npm init -y
npm install commander
npm install -D typescript @types/node tsx
Create a TypeScript configuration file (tsconfig.json):
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Add the following to your package.json:
{
"name": "my-cli-tool",
"version": "1.0.0",
"description": "CLI tool for file management",
"main": "dist/index.js",
"bin": {
"mytool": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
},
"keywords": ["cli", "file-management", "automation"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"commander": "^11.1.0",
"inquirer": "^9.2.0",
"ora": "^7.0.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"tsx": "^4.7.0"
}
}
💡 Tip: The bin field defines your CLI command name. Here we’ve set it to mytool.
Step 2: Build the Basic CLI Structure
Create src/index.ts with the basic structure:
#!/usr/bin/env node
import { Command } from 'commander';
import * as fs from 'fs';
import * as path from 'path';
const program = new Command();
program
.name('mytool')
.description('CLI tool for file management')
.version('1.0.0');
// File creation command
program
.command('create <filename>')
.description('Create a new file')
.option('-c, --content <text>', 'File content')
.action((filename: string, options: { content?: string }) => {
try {
const content = options.content || '// New file\n';
fs.writeFileSync(filename, content, 'utf-8');
console.log(`✅ File created: ${filename}`);
} catch (error) {
console.error(`❌ File creation failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
// File list command
program
.command('list [directory]')
.description('Display files in a directory')
.option('-a, --all', 'Include hidden files')
.action((directory: string = '.', options: { all?: boolean }) => {
try {
const files = fs.readdirSync(directory);
const filteredFiles = options.all
? files
: files.filter(file => !file.startsWith('.'));
console.log(`\n📁 Directory: ${path.resolve(directory)}\n`);
filteredFiles.forEach(file => {
const stats = fs.statSync(path.join(directory, file));
const icon = stats.isDirectory() ? '📂' : '📄';
console.log(` ${icon} ${file}`);
});
console.log(`\nTotal: ${filteredFiles.length} items\n`);
} catch (error) {
console.error(`❌ List operation failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
program.parse(process.argv);
⚠️ Important: The shebang line #!/usr/bin/env node at the top is required. Without it, the shell won’t execute your tool.
Step 3: Build and Test
Compile TypeScript to JavaScript:
npm run build
Use npm link to test locally:
npm link
Now you can use your CLI tool from the terminal:
# Create a file
mytool create test.js --content "console.log('Hello');"
# List files
mytool list
# List files including hidden ones
mytool list --all
# Show help
mytool --help
Production-Ready Tips
Error Handling
Always use try-catch blocks in your commands to handle errors gracefully. Providing clear error messages to users is crucial.
Input Validation
Use libraries like Zod to validate user input.
First, install Zod:
npm install zod
Example usage:
import { z } from 'zod';
const fileNameSchema = z.string().regex(/^[\w\-. ]+$/);
// Use in your command
const validatedName = fileNameSchema.parse(filename);
Interactive Prompts
For complex input scenarios, use inquirer to provide an interactive interface.
Installation:
npm install inquirer
Progress Indicators
For long-running operations, display a spinner using ora.
First, install ora:
npm install ora
Example usage:
import ora from 'ora';
const spinner = ora('Creating file...').start();
// Perform operation
spinner.succeed('File created successfully!');
Additional Considerations
Distribution
Publish your package to npm so anyone can install it with npm install -g my-cli-tool. Before publishing, ensure:
-
Unique package name (check availability on npm)
-
Comprehensive README.md (usage instructions and examples)
-
LICENSE file included
-
.npmignoreto exclude unnecessary files
Testing
CLI tools need testing too. Write unit tests using Vitest or Jest.
Cross-Platform Compatibility
Ensure your tool works on Windows, macOS, and Linux:
-
Use
path.join()for path separators -
Consider platform differences when handling file permissions
-
Use
cross-spawnwhen executing shell commands
Conclusion
With Node.js and Commander.js, you can quickly develop professional CLI tools. TypeScript-powered Node.js CLI development offers both type safety and productivity. Let’s recap what we covered:
-
Project setup: Creating a structured Node.js CLI project with TypeScript and Commander.js
-
Command implementation: Defining functionality with
command()andoption() -
Production tips: Error handling, input validation, and progress indicators
Next steps:
-
Try running the examples above yourself
-
Add commands to automate your own repetitive tasks
-
Publish to npm and share with your team
Series continuation:
Now that you’ve mastered Node.js CLI development, it’s time to apply it to real projects! In the next article, we’ll explore building an automated publishing system integrated with WordPress. Manage your blog content automatically with CLI tools.
Additional resources:
-
Commander.js official documentation
-
Node.js official documentation
-
TypeScript handbook
Have questions about Node.js CLI development? Leave a comment below!
💡 All code in this article has been tested and is production-ready. For more complex examples, check out various use cases in the project’s GitHub repository.
Leave A Comment