Guidelines for developing standalone Deno CLI scripts using TypeScript for troubleshooting, diagnostics, batch processing, and automation...
You are an expert in Deno and TypeScript development with deep knowledge of building standalone CLI scripts, batch processing tools, and diagnostic utilities using Deno's native TypeScript support and built-in tooling.
any type - create necessary types insteadisLoading, hasError,
canDeletetype over interface for object shapes in scriptsas constreadonly for immutable propertiesGuide: For projects with multiple scripts sharing types, organize types in a dedicated directory.
| Project Size | Recommendation |
|---|---|
| Single script | Keep types inline in the script |
| 2-3 scripts with shared types | Create types/index.ts |
| Larger projects | Create types/ with multiple files |
project/
āāā main.ts # Config, utilities, re-exports types
āāā types/
ā āāā index.ts # All shared type definitions
āāā scripts/
āāā script-a.ts # Imports types from main.ts or types/
āāā script-b.ts
// types/index.ts
/**
* Shared Type Definitions
*
* Domain types used across multiple scripts.
*/
// =============================================================================
// Domain Types
// =============================================================================
/** User record from database */
export type User = {
readonly id: string;
readonly name: string;
readonly email: string;
};
/** User with computed fields */
export type UserWithBalance = User & {
readonly balance: string;
};
// =============================================================================
// API Response Types
// =============================================================================
export type ApiResponse<T> = {
readonly success: boolean;
readonly data?: T;
readonly error?: string;
};
// =============================================================================
// Script-Specific Types (export for reuse)
// =============================================================================
/** Result of a batch operation */
export type BatchResult<T> =
| { success: true; data: T }
| { success: false; error: string };
// main.ts
import "@std/dotenv/load";
// Re-export all types for convenient imports
export type {
ApiResponse,
BatchResult,
User,
UserWithBalance,
} from "./types/index.ts";
// ... rest of main.ts (config, utilities)
// scripts/process-users.ts
import {
type BatchResult,
config,
type User,
type UserWithBalance,
} from "../main.ts";
// Script-specific types can stay inline if not shared
type ProcessingStats = {
total: number;
processed: number;
failed: number;
};
| Type Category | Naming Pattern | Example |
|---|---|---|
| Domain entities | PascalCase noun | User, Transaction |
| With additions | EntityWithX |
UserWithBalance |
| API responses | EntityResponse |
TransactionResponse |
| State objects | EntityState |
AirdropState |
| Results | OperationResult |
SubmitResult, FetchResult |
| Status enums | EntityStatus |
TransactionStatus |
Before implementing, clarify these with the user to determine which patterns to apply:
The following sections are templates and patterns to apply based on the user's answers above. Adapt them to the specific use case.
project/
āāā deno.json # Configuration, tasks, and imports
āāā .env # Environment variables (gitignored)
āāā .env.example # Environment variables template
āāā main.ts # Shared configuration and utilities
āāā scripts/
āāā <script-name>.ts # Script files
āāā ...
{
"tasks": {
"check": "deno fmt --check && deno check scripts/*.ts",
"<task-name>": "deno run --allow-net --allow-read --allow-write --allow-env scripts/<script-name>.ts"
},
"imports": {
"@std/cli": "jsr:@std/cli@1",
"@std/dotenv": "jsr:@std/dotenv@0.225"
}
}
Add imports based on needs:
# Always needed
deno add jsr:@std/dotenv
# If using CLI arguments
deno add jsr:@std/cli
# If reading/writing CSV
deno add jsr:@std/csv
Always run before committing:
deno fmt
deno check scripts/*.ts
# Or use task
deno task check
Guide: Always use @std/dotenv for environment variables. Never hardcode
secrets.
// main.ts
import "@std/dotenv/load";
// Environment selection (if user needs staging/production switching)
export const ENV = Deno.env.get("ENV") || "production";
export const isStaging = ENV === "staging";
// Validation helper
const assertEnv = (name: string): string => {
const value = Deno.env.get(name);
if (!value) {
console.error(`Error: ${name} environment variable is required`);
Deno.exit(1);
}
return value;
};
// Load required env vars
export const API_KEY = assertEnv("API_KEY");
// Environment-aware URLs (if needed)
export const API_BASE = isStaging
? "https://staging.api.example.com"
: "https://api.example.com";
// Environment-aware file naming (if user needs environment switching)
export const getInputFile = (baseName: string): string =>
isStaging ? `./${baseName}.staging.json` : `./${baseName}.production.json`;
export const getOutputFile = (baseName: string): string =>
isStaging ? `./${baseName}.staging.json` : `./${baseName}.production.json`;
.env.example:
ENV="production"
API_KEY="your_api_key_here"
Guide: Use arrow functions throughout. Organize with clear sections.
import "@std/dotenv/load";
import { parseArgs } from "@std/cli/parse-args";
// ============================================================
// CLI Arguments (if user wants CLI flags)
// ============================================================
const args = parseArgs(Deno.args, {
string: ["file", "output"],
boolean: ["dry-run"],
default: {
file: "input.json",
output: "./output",
"dry-run": false,
},
});
// ============================================================
// Configuration
// ============================================================
const API_KEY = Deno.env.get("API_KEY");
if (!API_KEY) {
console.error("Error: API_KEY required");
Deno.exit(1);
}
// ============================================================
// Types
// ============================================================
type InputRecord = {
id: string;
// ... fields based on user's data
};
type OutputRecord = {
id: string;
status: "success" | "failed" | "skipped";
// ... fields based on user's needs
};
// ============================================================
// Utilities
// ============================================================
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
// ============================================================
// Core Logic
// ============================================================
const processRecord = async (record: InputRecord): Promise<OutputRecord> => {
// Implementation based on user's requirements
};
// ============================================================
// Main
// ============================================================
const main = async (): Promise<void> => {
// Implementation
};
main();
Guide: Apply if user needs to process many items with controlled concurrency.
const BATCH_SIZE = 50; // Adjust based on user's answer
const INTER_BATCH_DELAY_MS = 500;
const processBatch = async <T, R>(
items: T[],
processor: (item: T, index: number, total: number) => Promise<R>,
startIndex: number,
totalItems: number,
): Promise<R[]> => {
const promises = items.map((item, i) =>
processor(item, startIndex + i, totalItems)
);
return Promise.all(promises);
};
const processAllInBatches = async <T, R>(
items: T[],
processor: (item: T, index: number, total: number) => Promise<R>,
): Promise<R[]> => {
const results: R[] = [];
const totalBatches = Math.ceil(items.length / BATCH_SIZE);
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
console.log(`\n--- Batch ${batchNum}/${totalBatches} ---`);
const batchResults = await processBatch(batch, processor, i, items.length);
results.push(...batchResults);
if (i + BATCH_SIZE < items.length) {
await sleep(INTER_BATCH_DELAY_MS);
}
}
return results;
};
Guide: Apply for any HTTP requests or external API calls.
const MAX_RETRIES = 5;
const INITIAL_BACKOFF_MS = 1000;
const withRetry = async <T>(
fn: () => Promise<T>,
label: string,
): Promise<T> => {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < MAX_RETRIES) {
const backoffMs = INITIAL_BACKOFF_MS * 2 ** (attempt - 1);
console.warn(
` [Retry ${attempt}/${MAX_RETRIES}] ${label} failed. Waiting ${backoffMs}ms...`,
);
await sleep(backoffMs);
}
}
}
throw lastError;
};
Guide: Apply if user wants the script to be resumable after interruption.
type ScriptState = {
lastUpdated: string;
environment: string;
summary: { total: number; success: number; failed: number; skipped: number };
results: OutputRecord[];
};
const STATE_FILE = "./output/state.json";
const loadState = async (): Promise<ScriptState | null> => {
try {
const data = await Deno.readTextFile(STATE_FILE);
return JSON.parse(data);
} catch {
return null;
}
};
const saveState = async (state: ScriptState): Promise<void> => {
await Deno.writeTextFile(STATE_FILE, JSON.stringify(state, null, 2));
};
// In main loop:
const main = async (): Promise<void> => {
const previousState = await loadState();
const alreadyProcessed = new Set(
previousState?.results
.filter((r) => r.status === "success")
.map((r) => r.id) || [],
);
const results: OutputRecord[] = previousState?.results || [];
for (const [i, record] of inputData.entries()) {
const progress = `[${i + 1}/${inputData.length}]`;
if (alreadyProcessed.has(record.id)) {
console.log(`${progress} ${record.id} - SKIPPED (already done)`);
continue;
}
// Process and save state after EACH operation
// ...
await saveState(currentState);
}
};
Guide: Apply based on user's input format answer.
const loadJsonInput = async <T>(filePath: string): Promise<T[]> => {
const content = await Deno.readTextFile(filePath);
return JSON.parse(content);
};
import { parse } from "@std/csv/parse";
const loadCsvInput = async <T>(
filePath: string,
columns: string[],
): Promise<T[]> => {
const content = await Deno.readTextFile(filePath);
return parse(content, { skipFirstRow: true, columns }) as T[];
};
const loadTextInput = async (filePath: string): Promise<string[]> => {
const content = await Deno.readTextFile(filePath);
return content.split("\n").map((line) => line.trim()).filter((line) => line);
};
Guide: Apply based on user's output format answer.
const writeJsonOutput = async <T>(filePath: string, data: T): Promise<void> => {
await Deno.writeTextFile(filePath, JSON.stringify(data, null, 2));
console.log(`Wrote JSON to ${filePath}`);
};
import { stringify } from "@std/csv/stringify";
const writeCsvOutput = async (
filePath: string,
columns: string[],
rows: Record<string, unknown>[],
): Promise<void> => {
const csv = stringify(rows, { columns });
await Deno.writeTextFile(filePath, csv);
console.log(`Wrote ${rows.length} rows to ${filePath}`);
};
const writeSummary = async (
filePath: string,
stats: Record<string, unknown>,
): Promise<void> => {
const summary = `
================================================================================
SUMMARY
================================================================================
Timestamp: ${new Date().toISOString()}
${Object.entries(stats).map(([k, v]) => `${k}: ${v}`).join("\n")}
================================================================================
`.trim();
await Deno.writeTextFile(filePath, summary);
console.log(summary);
};
Guide: Use consistent logging throughout.
// Progress: [current/total]
const progress = `[${i + 1}/${items.length}]`;
// Status markers
console.log(`${progress} ${id} - SUCCESS`);
console.log(`${progress} ${id} - FAILED: ${error}`);
console.log(`${progress} ${id} - SKIPPED (reason)`);
// Section dividers
console.log(
"================================================================================",
);
console.log("SECTION TITLE");
console.log(
"================================================================================",
);
// Batch progress
console.log(`\n--- Batch ${batchNum}/${totalBatches} ---`);
Guide: Always wrap in retry logic. Never hardcode API keys.
const fetchData = async (url: string): Promise<unknown> => {
return withRetry(async () => {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (response.status === 429) {
throw new Error("Rate limited");
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}, `Fetching ${url}`);
};
Guide: Apply for financial calculations to avoid floating-point errors.
const formatUnits = (raw: bigint, decimals: number): string => {
const divisor = BigInt(10 ** decimals);
const whole = raw / divisor;
const fraction = (raw % divisor).toString().padStart(decimals, "0");
return `${whole}.${fraction}`;
};
const parseUnits = (formatted: string, decimals: number): bigint => {
const [whole, fraction = ""] = formatted.split(".");
const paddedFraction = fraction.padEnd(decimals, "0").slice(0, decimals);
return BigInt(whole + paddedFraction);
};
Deno is secure by default. Request only necessary permissions:
# Run with specific permissions
deno run --allow-net --allow-read=./data --allow-env scripts/process.ts
# Permission flags
--allow-net=example.com # Network access to specific domains
--allow-read=./path # File read access
--allow-write=./path # File write access
--allow-env=API_KEY # Environment variable access
--allow-run=cmd # Subprocess execution
# Formatting (uses Deno defaults)
deno fmt
# Linting
deno lint
# Type checking
deno check scripts/*.ts
# Dependency inspection
deno info scripts/main.ts
# Compile to standalone executable
deno compile --allow-net --allow-read --allow-env scripts/main.ts
Guide: Apply when user needs automated tests for script utilities.
// scripts/utils_test.ts
import { assertEquals, assertRejects } from "@std/assert";
import { describe, it } from "@std/testing/bdd";
import { processRecord, validateInput } from "./utils.ts";
describe("processRecord", () => {
it("should process valid record", async () => {
const result = await processRecord({ id: "1", name: "test" });
assertEquals(result.status, "success");
});
it("should throw for invalid input", async () => {
await assertRejects(
() => processRecord({ id: "", name: "" }),
Error,
"Invalid input",
);
});
});
# Run tests
deno test --allow-net --allow-read
Deno embraces web standards. Use these APIs:
fetch() for HTTP requestsURL and URLSearchParams for URL manipulationTextEncoder / TextDecoder for encodingcrypto.subtle for cryptographyAbortController for request cancellationDeno.Command for subprocess execution# Format and check
deno task check
# Run with task
deno task <task-name>
# Run directly
deno run --allow-net --allow-read --allow-write --allow-env scripts/<script>.ts
# Switch environment
ENV=staging deno task <task-name>
# With CLI arguments
deno task <task-name> --file input.json --dry-run