In the previous tutorial, we learned about TypeScript configuration. Now let’s put everything together and build a complete CLI tool — a bookmark manager that stores, searches, and exports bookmarks from your terminal.

This is the final project of the TypeScript tutorial series. We will use Commander for argument parsing, Zod for validation, chalk for colors, and TypeScript for type safety throughout.

What We Are Building

A CLI tool called bm (bookmark manager) with these commands:

bm add "TypeScript Docs" https://typescriptlang.org --tags typescript,docs
bm list
bm list --tag typescript
bm search "docs"
bm delete <id>
bm export --format json
bm export --format csv

Project Setup

mkdir bookmark-cli
cd bookmark-cli
npm init -y

Install dependencies:

npm install commander chalk zod
npm install --save-dev typescript @types/node tsx

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

package.json

{
  "name": "bookmark-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "bm": "./dist/index.js"
  },
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Project Structure

src/
  index.ts          # Entry point — CLI commands
  types.ts          # Type definitions and Zod schemas
  store.ts          # File-based storage
  commands/
    add.ts          # Add bookmark command
    list.ts         # List bookmarks command
    search.ts       # Search bookmarks command
    delete.ts       # Delete bookmark command
    export.ts       # Export bookmarks command

Define Types and Schemas

// src/types.ts
import { z } from "zod";

export const BookmarkSchema = z.object({
  id: z.string(),
  title: z.string().min(1),
  url: z.string().url(),
  tags: z.array(z.string()).default([]),
  createdAt: z.string().datetime(),
});

export type Bookmark = z.infer<typeof BookmarkSchema>;

export const BookmarkStoreSchema = z.object({
  bookmarks: z.array(BookmarkSchema),
});

export type BookmarkStore = z.infer<typeof BookmarkStoreSchema>;

export const AddBookmarkInput = z.object({
  title: z.string().min(1, "Title is required"),
  url: z.string().url("Invalid URL format"),
  tags: z.array(z.string()).default([]),
});

export type AddBookmarkInputType = z.infer<typeof AddBookmarkInput>;

File-Based Storage

// src/store.ts
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { Bookmark, BookmarkStore, BookmarkStoreSchema } from "./types.js";

const STORE_PATH = path.join(os.homedir(), ".bookmarks.json");

function readStore(): BookmarkStore {
  try {
    const data = fs.readFileSync(STORE_PATH, "utf-8");
    return BookmarkStoreSchema.parse(JSON.parse(data));
  } catch {
    return { bookmarks: [] };
  }
}

function writeStore(store: BookmarkStore): void {
  fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
}

export function getAllBookmarks(): Bookmark[] {
  return readStore().bookmarks;
}

export function addBookmark(bookmark: Bookmark): void {
  const store = readStore();
  store.bookmarks.push(bookmark);
  writeStore(store);
}

export function deleteBookmark(id: string): boolean {
  const store = readStore();
  const index = store.bookmarks.findIndex(b => b.id === id);
  if (index === -1) return false;
  store.bookmarks.splice(index, 1);
  writeStore(store);
  return true;
}

export function searchBookmarks(query: string): Bookmark[] {
  const bookmarks = getAllBookmarks();
  const lower = query.toLowerCase();
  return bookmarks.filter(
    b =>
      b.title.toLowerCase().includes(lower) ||
      b.url.toLowerCase().includes(lower) ||
      b.tags.some(t => t.toLowerCase().includes(lower))
  );
}

export function getBookmarksByTag(tag: string): Bookmark[] {
  const bookmarks = getAllBookmarks();
  return bookmarks.filter(b =>
    b.tags.some(t => t.toLowerCase() === tag.toLowerCase())
  );
}

Command: Add Bookmark

// src/commands/add.ts
import crypto from "node:crypto";
import chalk from "chalk";
import { AddBookmarkInput, Bookmark } from "../types.js";
import { addBookmark } from "../store.js";

export function handleAdd(title: string, url: string, options: { tags?: string }): void {
  const tags = options.tags ? options.tags.split(",").map(t => t.trim()) : [];

  const result = AddBookmarkInput.safeParse({ title, url, tags });

  if (!result.success) {
    for (const issue of result.error.issues) {
      console.error(chalk.red(`Error: ${issue.message}`));
    }
    process.exit(1);
  }

  const bookmark: Bookmark = {
    id: crypto.randomUUID().slice(0, 8),
    title: result.data.title,
    url: result.data.url,
    tags: result.data.tags,
    createdAt: new Date().toISOString(),
  };

  addBookmark(bookmark);
  console.log(chalk.green(`Added: ${bookmark.title}`));
  console.log(chalk.gray(`  ID: ${bookmark.id}`));
  console.log(chalk.gray(`  URL: ${bookmark.url}`));
  if (bookmark.tags.length > 0) {
    console.log(chalk.gray(`  Tags: ${bookmark.tags.join(", ")}`));
  }
}

Command: List Bookmarks

// src/commands/list.ts
import chalk from "chalk";
import { Bookmark } from "../types.js";
import { getAllBookmarks, getBookmarksByTag } from "../store.js";

function formatBookmark(bookmark: Bookmark): void {
  console.log(chalk.bold(`  ${bookmark.title}`));
  console.log(chalk.blue(`  ${bookmark.url}`));
  console.log(chalk.gray(`  ID: ${bookmark.id}  Tags: ${bookmark.tags.join(", ") || "none"}`));
  console.log();
}

export function handleList(options: { tag?: string }): void {
  const bookmarks = options.tag
    ? getBookmarksByTag(options.tag)
    : getAllBookmarks();

  if (bookmarks.length === 0) {
    console.log(chalk.yellow("No bookmarks found."));
    return;
  }

  console.log(chalk.bold(`\nBookmarks (${bookmarks.length}):\n`));
  for (const bookmark of bookmarks) {
    formatBookmark(bookmark);
  }
}

Command: Search Bookmarks

// src/commands/search.ts
import chalk from "chalk";
import { searchBookmarks } from "../store.js";

export function handleSearch(query: string): void {
  const results = searchBookmarks(query);

  if (results.length === 0) {
    console.log(chalk.yellow(`No bookmarks matching "${query}".`));
    return;
  }

  console.log(chalk.bold(`\nResults for "${query}" (${results.length}):\n`));
  for (const bookmark of results) {
    console.log(chalk.bold(`  ${bookmark.title}`));
    console.log(chalk.blue(`  ${bookmark.url}`));
    console.log(chalk.gray(`  ID: ${bookmark.id}`));
    console.log();
  }
}

Command: Delete Bookmark

// src/commands/delete.ts
import chalk from "chalk";
import { deleteBookmark } from "../store.js";

export function handleDelete(id: string): void {
  const deleted = deleteBookmark(id);

  if (deleted) {
    console.log(chalk.green(`Deleted bookmark ${id}.`));
  } else {
    console.log(chalk.red(`Bookmark ${id} not found.`));
    process.exit(1);
  }
}

Command: Export Bookmarks

// src/commands/export.ts
import chalk from "chalk";
import { getAllBookmarks } from "../store.js";

type ExportFormat = "json" | "csv";

export function handleExport(options: { format: string }): void {
  const format = options.format as ExportFormat;
  const bookmarks = getAllBookmarks();

  if (bookmarks.length === 0) {
    console.log(chalk.yellow("No bookmarks to export."));
    return;
  }

  switch (format) {
    case "json":
      console.log(JSON.stringify(bookmarks, null, 2));
      break;

    case "csv": {
      // CSV escape: wrap in quotes and double any internal quotes
      const csvField = (v: string) => `"${v.replace(/"/g, '""')}"`;
      console.log("id,title,url,tags,createdAt");
      for (const b of bookmarks) {
        const row = [
          csvField(b.id),
          csvField(b.title),
          csvField(b.url),
          csvField(b.tags.join(";")),
          csvField(b.createdAt),
        ].join(",");
        console.log(row);
      }
      break;
    }

    default:
      console.error(chalk.red(`Unknown format: ${format}. Use "json" or "csv".`));
      process.exit(1);
  }
}

Entry Point — Wiring Everything Together

#!/usr/bin/env node
// src/index.ts

import { Command } from "commander";
import { handleAdd } from "./commands/add.js";
import { handleList } from "./commands/list.js";
import { handleSearch } from "./commands/search.js";
import { handleDelete } from "./commands/delete.js";
import { handleExport } from "./commands/export.js";

const program = new Command();

program
  .name("bm")
  .description("A type-safe bookmark manager for your terminal")
  .version("1.0.0");

program
  .command("add")
  .description("Add a new bookmark")
  .argument("<title>", "Bookmark title")
  .argument("<url>", "Bookmark URL")
  .option("-t, --tags <tags>", "Comma-separated tags")
  .action(handleAdd);

program
  .command("list")
  .description("List all bookmarks")
  .option("-t, --tag <tag>", "Filter by tag")
  .action(handleList);

program
  .command("search")
  .description("Search bookmarks")
  .argument("<query>", "Search query")
  .action(handleSearch);

program
  .command("delete")
  .description("Delete a bookmark")
  .argument("<id>", "Bookmark ID")
  .action(handleDelete);

program
  .command("export")
  .description("Export bookmarks")
  .option("-f, --format <format>", "Export format (json or csv)", "json")
  .action(handleExport);

program.parse();

Building and Running

Development

npx tsx src/index.ts add "TypeScript Docs" https://typescriptlang.org --tags typescript,docs
npx tsx src/index.ts list
npx tsx src/index.ts search "typescript"

Production Build

npx tsc
node dist/index.js list

Install Globally

npm link
bm add "GitHub" https://github.com --tags dev,code
bm list

Publishing to npm

Update package.json:

{
  "name": "@kemalcodes/bookmark-cli",
  "version": "1.0.0",
  "bin": {
    "bm": "./dist/index.js"
  },
  "files": ["dist"],
  "scripts": {
    "prepublishOnly": "tsc"
  }
}

Make sure the entry point has the shebang line:

#!/usr/bin/env node

Publish:

npm publish --access public

Now anyone can install and use your tool:

npx @kemalcodes/bookmark-cli add "TypeScript" https://typescriptlang.org

What We Covered in This Series

This was the final tutorial in the TypeScript series. Here is what you learned across all 25 tutorials:

Foundations (1-8): Types, functions, interfaces, unions, enums, type narrowing Intermediate (9-16): Generics, classes, modules, utility types, mapped types, template literals, error handling, async/await Practical (17-22): React, Node.js, Next.js, testing, Zod, tRPC Advanced (23-25): Advanced patterns, tsconfig, and this CLI project

You now have the knowledge to build type-safe applications at every level — from small scripts to full-stack production applications.