A production-ready TypeScript monorepo template for building type-safe CLI applications with extensibility for web applications.
- 🎯 Type-Safe Everything: Strict TypeScript + Zod runtime validation + Optique discriminated unions
- 📦 Monorepo Architecture: pnpm workspace for code sharing between CLI and future web apps
- 🔧 Modern Tooling: tsup for building, Biome for quality, Jest for testing
- ⚡ Type-Safe CLI: Optique with automatic type inference and shell completion
- 🐛 Flexible Debugging: debug module with namespace-based filtering
- ⚙️ Multi-Source Config: Defaults → Config file → Environment → CLI args
- ✨ Excellent DX: Hot reload, auto-completion, helpful error messages
- 🏗️ Production Ready: Quality gates, strict linting, comprehensive validation
| Purpose | Library | Version |
|---|---|---|
| CLI Parser | Optique | 0.6.0 |
| Debugging | debug | 4.4.0 |
| Configuration | Conf | 15.0.2 |
| Schema Validation | Zod | 4.1.11 |
| Env Validation | envalid | 8.1.0 |
| Lint/Format | Biome | 2.2.5 |
| Build | tsup | 8.5.0 |
| Test | Jest | 29.7.0 |
| TypeScript | TypeScript | 5.9.3 |
| Package Manager | pnpm | 9.15.4+ |
typescript-cli-template/
├── apps/
│ ├── cli/ # CLI application
│ │ ├── src/
│ │ │ ├── cli.ts # Main entry point
│ │ │ └── commands/ # Command implementations
│ │ │ ├── config.ts # Config management
│ │ │ └── process.ts # File processing
│ │ └── package.json
│ └── web/ # (Future) Web application
├── packages/
│ ├── core/ # Platform-agnostic business logic
│ │ ├── src/models/ # Zod schemas & types
│ │ └── src/services/ # Business services
│ ├── config/ # Configuration management
│ │ ├── src/env.ts # Environment validation (envalid)
│ │ ├── src/schemas.ts # Config schemas (Zod)
│ │ └── src/store.ts # Config storage (Conf)
│ └── shared/ # Shared utilities
│ ├── src/exceptions.ts # Custom exceptions
│ ├── src/types/ # Common types
│ └── src/utils/ # Utilities (string, array, object)
├── scripts/
│ └── check.sh # Quality checks runner
├── CLAUDE.md # Development guidelines
├── LIBRARIES.md # Core libraries guide
└── package.json # Root workspace config
This template uses myapp as a placeholder. You must rename it to your project name before starting development.
See CUSTOMIZATION.md for detailed instructions.
Quick customization:
# Find all files that need customization
grep -r "TEMPLATE_CUSTOMIZE" . --exclude-dir=node_modules --exclude-dir=dist
# Replace myapp with your project name
# 1. Package names: @myapp/* → @yourproject/*
# 2. Program name: myapp → yourproject
# 3. Env var prefix: MYAPP_ → YOURPROJECT_
# 4. Logger categories: ['myapp', ...] → ['yourproject', ...]# Clone the repository
git clone <your-repo-url>
cd typescript-cli-template
# Install dependencies
pnpm install
# Build all packages
pnpm buildInstall the CLI globally to use myapp command from anywhere:
# Install globally (builds and links CLI)
pnpm install:global
# Now use from anywhere
myapp --version
myapp config path
myapp process input.txt -o output.txt
# Uninstall when done
pnpm uninstall:global# Development mode (with hot reload)
pnpm --filter @myapp/cli dev config path
# Production mode (if installed globally)
myapp config path
# Production mode (run built version directly)
node apps/cli/dist/cli.js config path# Run CLI with hot reload
pnpm --filter @myapp/cli dev [command] [args...]
# Install/update global CLI
pnpm install:global
# Uninstall global CLI
pnpm uninstall:global
# Type check all packages
pnpm typecheck
# Format code
pnpm format
# Lint code
pnpm lint
# Run all quality checks (typecheck + format + lint + build)
./scripts/check.sh
# Build all packages
pnpm build
# Build specific package
pnpm --filter @myapp/core build
# Clean build outputs
pnpm clean- Make changes to code
- Run
pnpm typecheckto check types - Run
./scripts/check.shto verify quality - Auto-fix issues:
pnpm biome check --write .
The examples below assume you've installed the CLI globally with pnpm install:global. For development mode, prefix commands with pnpm --filter @myapp/cli dev.
# View config file path
myapp config path
# Output: /Users/user/Library/Preferences/myapp/config.json
# Set a configuration value
myapp config set apiKey "sk-1234567890"
# Output: ✓ Configuration saved
# Get a configuration value
myapp config get apiKey
# Output: sk-1234567890
# List all configuration
myapp config list
# Output: { "apiKey": "sk-1234567890" }
# Remove a configuration value
myapp config unset apiKey
# Output: ✓ Configuration value removed# Process a file
myapp process input.txt --output result.txt
# Output:
# ✓ Processing completed
# Processed: 100
# Skipped: 5
# Errors: 0
# Duration: 102.17ms
# Output: result.txt
# Force overwrite existing files
myapp process file.txt -o output.txt --force
# Short flags
myapp process file.txt -o output.txt -fUse the DEBUG environment variable to enable debug output:
# Enable all debug output
DEBUG=myapp:* myapp process file.txt -o output.txt
# Output:
# ✓ Processing completed
# ...
# 2025-10-17 myapp:core:processor Starting processing {...}
# 2025-10-17 myapp:core:processor Processing with options {...}
# 2025-10-17 myapp:core:processor Processing completed {...}
# Enable specific namespace only
DEBUG=myapp:cli:* myapp config set key value
DEBUG=myapp:core:* myapp process file.txt -o output.txt
# Enable multiple namespaces
DEBUG=myapp:cli:config,myapp:core:processor myapp process file.txt -o output.txt# Show version
myapp --version
# Output: 0.1.0
# Show help
myapp --help
# Show help for specific command
myapp config --help
# Shell completion (bash/zsh/fish)
myapp completion bashConfiguration values are loaded from multiple sources in this order (later sources override earlier):
- Schema Defaults (defined in Zod schemas)
- Config File (
~/.config/myapp/config.jsonorMYAPP_CONFIG_DIR) - Environment Variables (prefixed with
MYAPP_) - CLI Arguments (command-line flags)
All environment variables are validated at startup using envalid.
| Variable | Type | Default | Description |
|---|---|---|---|
MYAPP_NO_COLOR |
boolean | false |
Disable colored output |
MYAPP_CONFIG_DIR |
string | OS-specific | Custom config directory path |
MYAPP_ENDPOINT |
string | https://api.example.com |
API endpoint URL |
MYAPP_API_KEY |
string | - | API key for authentication |
MYAPP_TIMEOUT |
number | 30000 |
Request timeout (milliseconds) |
MYAPP_TELEMETRY |
boolean | true |
Enable telemetry collection |
DEBUG |
string | - | Enable debug output (e.g., myapp:*) |
# Set environment variables
export MYAPP_ENDPOINT=https://custom.api.com
export MYAPP_API_KEY=your-secret-key
export DEBUG=myapp:* # Enable debug output
# Run with custom config
myapp process data.txt -o out.txtCLI application built with Optique for type-safe command parsing.
Key Features:
- Automatic type inference from parsers
- Built-in shell completion support
- Discriminated unions for command handling
- Helpful error messages
Platform-agnostic business logic that can be shared across CLI and web applications.
import { ProcessorService } from '@myapp/core';
const processor = new ProcessorService();
const result = await processor.process({
file: 'input.txt',
output: 'output.txt',
force: true,
options: {
batchSize: 100,
maxRetries: 3,
},
});
console.log(`Processed: ${result.processed}`);Type-safe configuration management with multi-source loading.
import { loadConfig, store, ENV } from '@myapp/config';
// Load configuration from all sources
const config = loadConfig({ endpoint: 'https://custom.api' });
// Get/set configuration values (supports dot notation)
store.set('log.level', 'debug');
store.set('preferences.theme', 'dark');
const logLevel = store.get<string>('log.level');
// Access environment variables (validated with envalid)
console.log(ENV.MYAPP_NO_COLOR); // Type-safe!
// Check config file location
console.log(store.path);Common utilities, types, and exceptions used across all packages.
// Exceptions
import { AppError, ValidationError, ConfigError, FileError } from '@myapp/shared';
throw new ValidationError('Invalid input', 'Please check your data format');
// Types
import type { Brand, JsonValue, JsonObject } from '@myapp/shared/types';
type UserId = Brand<string, 'UserId'>;
// Utilities
import {
deepMerge,
getByPath,
setByPath,
capitalize,
camelCase,
chunk,
unique,
} from '@myapp/shared/utils';
const obj = { user: { name: 'John' } };
const name = getByPath(obj, 'user.name'); // 'John'
const merged = deepMerge({ a: 1 }, { b: 2 }); // { a: 1, b: 2 }- CLAUDE.md - Development principles and coding standards
- LIBRARIES.md - Core libraries guide and troubleshooting
-
Strong Typing is Mandatory
- ❌ Never use
any - ✅ Zod-first: Define schemas, infer types
- ✅ Type everything: All params and returns
- ❌ Never use
-
Code Quality Standards
- Files: ≤ 300 lines
- Functions: ≤ 50 lines, ≤ 5 parameters
- Complexity: ≤ 10 (cyclomatic)
- Testing: Write tests for all features
-
Best Practices
- Fail fast with validation at boundaries
- Explicit over implicit
- Immutability by default
- Use
debugfor development debugging, console for user output
import { z } from 'zod';
// 1. Define schema first
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// 2. Infer type from schema
type User = z.infer<typeof UserSchema>;
// 3. Use for validation
function createUser(data: unknown): User {
return UserSchema.parse(data); // Runtime validation!
}
// 4. Safe parsing
function tryCreateUser(data: unknown): User | null {
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error('Validation failed:', result.error);
return null;
}
return result.data;
}Run comprehensive quality checks before committing:
# Run all checks at once
./scripts/check.sh
# Or run individually
pnpm typecheck # TypeScript strict mode
pnpm biome format . # Check formatting
pnpm biome lint . # Run linter
pnpm biome check . # Format + lint + organize imports
pnpm build # Build all packages# Auto-format code
pnpm biome format --write .
# Auto-fix linting issues
pnpm biome lint --write .
# Auto-fix everything (format + lint + organize imports)
pnpm biome check --write .- ✅ TypeScript strict mode enabled
- ✅ Cyclomatic complexity ≤ 10
- ✅ No unused imports/variables
- ✅ No
anytypes - ✅ Proper error handling
- ✅ All schemas validated with Zod
This monorepo is designed for easy extension to web applications:
# Create new web app
mkdir -p apps/web
cd apps/web
pnpm init
# Install web framework (e.g., Next.js, Vite + React)
pnpm add next react react-dom
# Add workspace dependencies
pnpm add @myapp/core @myapp/shared// apps/web/src/services/processor.ts
import { ProcessorService } from '@myapp/core';
import createDebug from 'debug';
const debug = createDebug('myapp:web:processor');
export async function processInBrowser(data: ProcessInput) {
debug('Processing in browser', data);
const processor = new ProcessorService();
const result = await processor.process(data);
debug('Processing complete', result);
return result;
}- Create command file in
apps/cli/src/commands/:
import { command, object, constant, argument } from '@optique/core/parser';
import { string } from '@optique/core/valueparser';
import type { InferValue } from '@optique/core/parser';
export const myCommand = command(
'my-command',
object({
command: constant('my-command' as const),
arg: argument(string({ metavar: 'ARG' })),
}),
);
export type MyCommand = InferValue<typeof myCommand>;
export function handleMyCommand(cmd: MyCommand): void {
console.log(`Handling: ${cmd.arg}`);
}- Register in
apps/cli/src/cli.ts:
import { myCommand, handleMyCommand } from './commands/my-command.js';
const parser = or(configCommand, processCommand, myCommand);
// In main():
if (result.command === 'my-command') {
handleMyCommand(result);
}- Create package structure:
mkdir -p packages/my-package/src
cd packages/my-package
pnpm init- Configure
package.json:
{
"name": "@myapp/my-package",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
}
}- Add Vite config with SSR support for Node.js:
// vite.config.ts
import { resolve } from "node:path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
plugins: [dts({ rollupTypes: true })],
build: {
ssr: true, // Important: Enables proper Node.js module handling
lib: {
entry: resolve(__dirname, "src/index.ts"),
formats: ["es"],
},
target: "node20",
sourcemap: true,
minify: false,
rollupOptions: {
external: [/^node:/, /* add external deps */],
},
},
});- Use in other packages:
pnpm add @myapp/my-package --filter @myapp/cli- Read the essential guides:
- CLAUDE.md - Development guidelines and coding standards
- LIBRARIES.md - Core libraries usage and troubleshooting
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes following the guidelines
- Run quality checks:
./scripts/check.sh - Commit with clear messages
- Open a pull request
This project uses several specialized libraries. For detailed usage, troubleshooting, and best practices, see:
LIBRARIES.md - Comprehensive guide covering:
- Optique (CLI parsing)
- debug (Development debugging)
- Zod (Schema validation)
- Conf (Configuration storage)
- envalid (Environment validation)
- tsup (Build tool)
- Biome (Linting/Formatting)
When working with these libraries, use these tools in order:
-
Context7 MCP (Most accurate) - Get latest API docs
resolve-library-id <library-name> get-library-docs <library-id> --topic "<topic>"
-
Exa MCP (Code examples) - Find real-world usage
get-code-context-exa "<library> <feature> examples" -
Web Search (Guides) - Find tutorials and guides
web-search "<library> documentation"
MIT
Built with TypeScript, Optique, and ❤️