Deno Core Best Practices
When to Use This Skill
Use this skill for ALL Deno TypeScript development:
- •Setting up new Deno projects
- •Writing Deno applications or libraries
- •Configuring build, test, and deployment
- •Working with dependencies and imports
Core Deno Philosophy
One Tool, Zero Dependencies
- •Deno is the only tool you need for TypeScript development
- •Built-in tooling: typecheck, lint, format, test, coverage, benchmark
- •Avoid
node_modulesat all costs - reduce supply chain attack surface - •No need for: tsc, eslint, prettier, jest, vitest, webpack, etc.
TypeScript Excellence
- •Strict TypeScript adherence - not just "TS support"
- •Bleeding-edge TypeScript features by default - no flags, no config needed
- •No compilation step - just run your code
- •Target ES2024+ with Stage 3 TC39 proposals
Security First
- •Explicit permissions model (no implicit file system or network access)
- •Supply chain security through minimal external dependencies
- •First-class support for modern security patterns
Language & Compiler
TypeScript Configuration
- •Do not use
tsconfig.json- Deno usesdeno.json(c)as the single source of truth - •Type-checking powered by
deno check/deno test- do not rely on externaltsc - •Default module format is ESM only - no CommonJS interop
- •Prefer Deno's runtime-provided types (
Deno.*, Web APIs, Fetch, URLPattern) over polyfills
Strictest Compiler Settings
Always use the strictest possible settings in deno.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}
Configuration & Tasks
deno.json - Single Source of Truth
Use deno.json or deno.jsonc as the single configuration file for:
- •Compiler options
- •Linting and formatting rules
- •Tasks (script aliases)
- •Import maps (dependency management)
- •Exclusions
Complete Configuration Example:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"tasks": {
"dev": "deno run --watch --allow-net --allow-read --allow-env src/main.ts",
"test": "deno test --allow-net --allow-read --allow-env --coverage=coverage src/",
"test:unit": "deno test --allow-net --allow-read --allow-env --coverage=coverage src/",
"test:e2e": "deno test --allow-net --allow-read --allow-env tests/e2e/",
"test:watch": "deno test --allow-net --allow-read --allow-env --watch --fail-fast",
"coverage": "deno coverage coverage --html",
"check": "deno check $(find src -name '*.ts' -not -name '*.sql')",
"lint": "deno lint",
"fmt": "deno fmt"
},
"imports": {
"@/": "./src/",
"@/domain/": "./src/domain/",
"@/infrastructure/": "./src/infrastructure/",
"@/application/": "./src/application/",
"@std/assert": "jsr:@std/assert@^1.0.14",
"@std/fs": "jsr:@std/fs@^1.0.19",
"@std/testing": "jsr:@std/testing@^1.0.15",
"@std/ulid": "jsr:@std/ulid@1",
"zod": "npm:zod@^3.23.8"
},
"exclude": [
"coverage/",
"node_modules/"
],
"lock": true
}
Essential Tasks
Define these deno task aliases at minimum:
- •
dev- Development with watch mode - •
test,test:watch- Testing - •
coverage- Generate coverage reports - •
check- Type-check all files - •
lint,fmt- Code quality
Lockfile Management
- •Always commit
deno.lockto version control - •Run with
--lock=deno.lock --lock-write=falsein CI - •Update lockfile:
deno cache --lock=deno.lock --lock-write
Imports & Module Resolution
Import Strategy
CRITICAL: Never use direct JSR/npm imports in source files. All external dependencies MUST be declared in deno.json import map.
Import Order in Source Files:
// 1. Standard library imports (via import map)
import { assertEquals } from "@std/assert";
// 2. Third-party imports (via import map)
import { z } from "zod";
// 3. Internal imports (absolute paths using import map)
import { Agent } from "@/domain/agent.ts";
// 4. Relative imports (only within same module/context)
import { validatePrompt } from "./validation.ts";
Dependency Source Priority
Use sources in this order:
- •
jsr:registry (first choice for TypeScript modules)json"@std/assert": "jsr:@std/assert@^1.0.14"
- •
npm:specifier (when needed; prefer ESM-compatible)json"zod": "npm:zod@^3.23.8"
- •
URL imports (rarely needed with import maps)
Version Pinning
CRITICAL: Version pin all external imports. No floating @latest in committed code.
{
"imports": {
"zod": "npm:zod@^3.23.8", // GOOD - pinned
"zod": "npm:zod", // BAD - no version
"@std/assert": "jsr:@std/assert@1" // GOOD - pinned
}
}
Internal Path Aliases
Use import map aliases for clean internal imports:
{
"imports": {
"@/": "./src/",
"@/domain/": "./src/domain/",
"@/infrastructure/": "./src/infrastructure/"
}
}
// GOOD - Clean, refactor-safe
import { Agent } from "@/domain/agent.ts";
// BAD - Brittle relative paths
import { Agent } from "../../../domain/agent.ts";
Type-Only Imports
Use type-only imports when importing types:
import type { Agent } from "@/domain/agent.ts";
import type { z } from "zod";
Testing
Test Organization
Unit Tests - Co-located with Source:
src/
└── domain/
├── agent.ts
└── agent.test.ts # Unit tests next to code
Integration & E2E Tests - Separate Directory:
tests/
├── integration/
│ └── openai_provider.test.ts
└── e2e/
└── workflow.test.ts
Why Co-location:
- •Discoverability - tests next to code
- •Maintenance - easy to keep in sync
- •Deno convention - follows
deno testdiscovery
Coverage Requirements
Non-Negotiable:
- •Line coverage: 80%+ - MUST be met
- •Branch coverage: 60-80% - MUST be met
deno test --coverage=coverage deno coverage coverage --html
Test Structure
Always use explicit AAA (Arrange-Act-Assert):
import { assertEquals } from "@std/assert";
Deno.test("agent should process valid input", () => {
// Arrange
const agent = new Agent({ name: "TestAgent" });
const input = "Hello, world!";
// Act
const result = agent.process(input);
// Assert
assertEquals(result.status, "success");
});
Test Development
Red-Green-Refactor with fast feedback:
# Watch mode with fail-fast deno test --watch --fail-fast # Run specific file deno test src/domain/agent.test.ts --watch
Deterministic Tests
CRITICAL: All tests must be deterministic.
Test Flakiness Policy:
- •Flakiness = highest priority bug
- •Never ignore, retry, or "fix" with delays
- •Action: investigate, quarantine, fix
- •Do NOT merge flaky tests
Use stable seeds and fixtures:
import { FakeTime } from "@std/testing/time";
Deno.test("timer test", () => {
using time = new FakeTime();
// Deterministic time control
time.tick(1000);
});
Test File Naming
All test files must end with .test.ts:
agent.test.ts # GOOD agent_test.ts # BAD agent.spec.ts # BAD
Testing Tools
- •Use
@std/assertfor assertions - •Use
@std/testing/mockfor test doubles - •Use
@std/testing/timefor time control - •Do NOT use Jest - use Deno's built-in runner
Permissions & Security
Principle of Least Privilege
Default to minimum required permissions:
# BAD deno run --allow-all script.ts # GOOD deno run --allow-read=./data --allow-net=api.example.com script.ts
Common Permission Flags
--allow-read[=<path>] # File system read --allow-write[=<path>] # File system write --allow-net[=<domain>] # Network access --allow-env[=<var>] # Environment variables --allow-run[=<program>] # Subprocess execution
Document Required Permissions
/**
* Fetches data from API and caches locally.
*
* Required permissions:
* - --allow-net=api.example.com
* - --allow-read=./cache
* - --allow-write=./cache
*/
export async function fetchData(): Promise<Data> {
// ...
}
Secrets Management
// BAD - Hardcoded
const apiKey = "sk-1234";
// GOOD - From environment
const apiKey = Deno.env.get("API_KEY");
if (!apiKey) {
throw new Error("API_KEY required");
}
Run with: deno run --allow-env=API_KEY script.ts
Anti-Patterns to Avoid
Import Anti-Patterns
// BAD - Direct JSR/npm imports in source
import { z } from "npm:zod@^3.23.8";
// GOOD - Use import map
import { z } from "zod";
// BAD - Floating versions "zod": "npm:zod" // GOOD - Pinned versions "zod": "npm:zod@^3.23.8"
Node.js Anti-Patterns
// BAD - Node.js APIs
const fs = require("fs");
import * as fs from "node:fs";
// GOOD - Deno APIs
await Deno.readTextFile("file.txt");
Testing Anti-Patterns
// BAD - Unnecessary delay
await new Promise(r => setTimeout(r, 100));
// GOOD - Deterministic time
import { FakeTime } from "@std/testing/time";
using time = new FakeTime();
time.tick(100);
Permission Anti-Patterns
# BAD - Overly broad deno run --allow-all script.ts # GOOD - Specific deno run --allow-read=./data script.ts
Async Anti-Patterns
// BAD - Unnecessary async
async function validate(input: string): Promise<boolean> {
return input.length > 0;
}
// GOOD - Remove async if no await
function validate(input: string): boolean {
return input.length > 0;
}
Quick Command Reference
Development
# Run with watch deno run --watch src/main.ts # Type-check deno check src/**/*.ts # Format deno fmt # Lint deno lint
Testing
# Run all tests deno test # With coverage deno test --coverage=coverage deno coverage coverage --html # Watch mode deno test --watch --fail-fast
Dependencies
# Update dependencies deno cache --reload # Update lockfile deno cache --lock=deno.lock --lock-write
Tasks
# Run tasks from deno.json deno task dev deno task test deno task coverage
Key Principles Summary
- •One tool - Deno replaces tsc, eslint, prettier, jest
- •Security first - Explicit permissions, minimal dependencies
- •Import maps - All deps in
deno.json, never direct imports - •Version pinning - No floating versions
- •Co-located tests - Unit tests next to source
- •80%/60% coverage - Line/branch, non-negotiable
- •No flakiness - Highest priority, never ignore
- •AAA pattern - Explicit in every test
- •Least privilege - Minimal permissions
- •ESM only - No CommonJS
Additional Resources
- •Deno Manual: https://docs.deno.com/
- •Deno Standard Library: https://jsr.io/@std
- •JSR Registry: https://jsr.io/