| name | deno-scripting |
| description | Guidelines for developing standalone Deno CLI scripts using TypeScript for troubleshooting, diagnostics, batch processing, and automation. Use when creating CLI tools, data processing scripts, reports, migration utilities, or any standalone TypeScript script running on Deno. |
Deno CLI Scripting
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.
TypeScript General Guidelines
Basic Principles
- Use English for all code and documentation
- Always declare types for variables and functions (parameters and return
values)
- Avoid using
any type - create necessary types instead
- Use JSDoc to document public classes and methods
- Write concise, maintainable, and technically accurate code
- Use functional and declarative programming patterns
- No configuration needed - Deno runs TypeScript natively
Nomenclature
- Use PascalCase for types and interfaces
- Use camelCase for variables, functions, and methods
- Use kebab-case for file and directory names
- Use UPPERCASE for environment variables
- Use descriptive variable names with auxiliary verbs:
isLoading, hasError,
canDelete
- Start each function with a verb
Functions
- Write short functions with a single purpose
- Use arrow functions for simple operations and consistency
- Use async/await for asynchronous operations
- Prefer the RO-RO pattern (Receive Object, Return Object) for multiple
parameters
Types and Interfaces
- Prefer
type over interface for object shapes in scripts
- Avoid enums; use const objects with
as const
- Use Zod for runtime validation when needed
- Use
readonly for immutable properties
Type Organization
Guide: For projects with multiple scripts sharing types, organize types in a
dedicated directory.
When to Use Separate Type Files
| 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 |
Directory Structure
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
Type File Pattern
export type User = {
readonly id: string;
readonly name: string;
readonly email: string;
};
export type UserWithBalance = User & {
readonly balance: string;
};
export type ApiResponse<T> = {
readonly success: boolean;
readonly data?: T;
readonly error?: string;
};
export type BatchResult<T> =
| { success: true; data: T }
| { success: false; error: string };
Re-exporting from main.ts
import "@std/dotenv/load";
export type {
ApiResponse,
BatchResult,
User,
UserWithBalance,
} from "./types/index.ts";
Importing in Scripts
import {
type BatchResult,
config,
type User,
type UserWithBalance,
} from "../main.ts";
type ProcessingStats = {
total: number;
processed: number;
failed: number;
};
Type Naming Conventions
| 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 |
Questions to Ask First
Before implementing, clarify these with the user to determine which patterns to
apply:
- Input format: "What is the input format - JSON file, CSV file, or text
file with one item per line?"
- Output format: "Should the output be JSON, CSV, or both? Do you need a
human-readable summary?"
- Environment: "Will this run against staging/testnet or
production/mainnet? Do you need environment switching?"
- Batch processing: "How many items should be processed in parallel per
batch? (default: 50)"
- Resumability: "Should the script be resumable if interrupted? (saves
state after each operation)"
- Dry run: "Do you want a --dry-run flag to preview without making
changes?"
Guides
The following sections are templates and patterns to apply based on the
user's answers above. Adapt them to the specific use case.
Project Structure
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
└── ...
deno.json Configuration
{
"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:
deno add jsr:@std/dotenv
deno add jsr:@std/cli
deno add jsr:@std/csv
Quality Checks
Always run before committing:
deno fmt
deno check scripts/*.ts
deno task check
Environment Configuration
Guide: Always use @std/dotenv for environment variables. Never hardcode
secrets.
import "@std/dotenv/load";
export const ENV = Deno.env.get("ENV") || "production";
export const isStaging = ENV === "staging";
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;
};
export const API_KEY = assertEnv("API_KEY");
export const API_BASE = isStaging
? "https://staging.api.example.com"
: "https://api.example.com";
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"
Script Structure
Guide: Use arrow functions throughout. Organize with clear sections.
import "@std/dotenv/load";
import { parseArgs } from "@std/cli/parse-args";
const args = parseArgs(Deno.args, {
string: ["file", "output"],
boolean: ["dry-run"],
default: {
file: "input.json",
output: "./output",
"dry-run": false,
},
});
const API_KEY = Deno.env.get("API_KEY");
if (!API_KEY) {
console.error("Error: API_KEY required");
Deno.exit(1);
}
type InputRecord = {
id: string;
};
type OutputRecord = {
id: string;
status: "success" | "failed" | "skipped";
};
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
const processRecord = async (record: InputRecord): Promise<OutputRecord> => {
};
const main = async (): Promise<void> => {
};
main();
Batch Processing
Guide: Apply if user needs to process many items with controlled
concurrency.
const BATCH_SIZE = 50;
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;
};
Retry with Exponential Backoff
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;
};
Resumability Pattern
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));
};
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;
}
await saveState(currentState);
}
};
Reading Input Files
Guide: Apply based on user's input format answer.
JSON Input
const loadJsonInput = async <T>(filePath: string): Promise<T[]> => {
const content = await Deno.readTextFile(filePath);
return JSON.parse(content);
};
CSV Input
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[];
};
Text Input (one item per line)
const loadTextInput = async (filePath: string): Promise<string[]> => {
const content = await Deno.readTextFile(filePath);
return content.split("\n").map((line) => line.trim()).filter((line) => line);
};
Writing Output Files
Guide: Apply based on user's output format answer.
JSON Output
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}`);
};
CSV Output
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}`);
};
Text Summary
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);
};
Logging Format
Guide: Use consistent logging throughout.
const progress = `[${i + 1}/${items.length}]`;
console.log(`${progress} ${id} - SUCCESS`);
console.log(`${progress} ${id} - FAILED: ${error}`);
console.log(`${progress} ${id} - SKIPPED (reason)`);
console.log(
"================================================================================",
);
console.log("SECTION TITLE");
console.log(
"================================================================================",
);
console.log(`\n--- Batch ${batchNum}/${totalBatches} ---`);
HTTP Requests
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}`);
};
BigInt for Token Amounts
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);
};
Security Model
Deno is secure by default. Request only necessary permissions:
deno run --allow-net --allow-read=./data --allow-env scripts/process.ts
--allow-net=example.com
--allow-read=./path
--allow-write=./path
--allow-env=API_KEY
--allow-run=cmd
Built-in Tooling
deno fmt
deno lint
deno check scripts/*.ts
deno info scripts/main.ts
deno compile --allow-net --allow-read --allow-env scripts/main.ts
Testing with Built-in Test Runner
Guide: Apply when user needs automated tests for script utilities.
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",
);
});
});
deno test --allow-net --allow-read
Web Standards
Deno embraces web standards. Use these APIs:
fetch() for HTTP requests
URL and URLSearchParams for URL manipulation
TextEncoder / TextDecoder for encoding
crypto.subtle for cryptography
AbortController for request cancellation
Performance Tips
- Use web streams for large file processing
- Process items in batches to avoid memory issues
- Use
Deno.Command for subprocess execution
- Compile to standalone executable for distribution
Run Commands
deno task check
deno task <task-name>
deno run --allow-net --allow-read --allow-write --allow-env scripts/<script>.ts
ENV=staging deno task <task-name>
deno task <task-name> --file input.json --dry-run