Deno Cursor Rules: Modern JavaScript Runtime

Cursor rules for Deno covering secure-by-default patterns, native TypeScript, ESM imports, Deno KV, Deno Deploy, deno.json config, testing with Deno.test, and Fresh framework conventions.

June 10, 2026by PromptGenius Team
denocursor-rulestypescriptruntimebackendfresh
Deno Cursor Rules: Modern JavaScript Runtime

Overview

Deno is a secure-by-default JavaScript/TypeScript runtime with native TypeScript support, built-in tooling, and web-standard APIs. These cursor rules enforce permission flags, ESM imports with mandatory file extensions, Deno KV, Deno Deploy patterns, deno.json configuration patterns, the standard library, and the Fresh web framework conventions so AI assistants generate secure, idiomatic Deno code that runs without node_modules.

Note:

Enforces permission flags (--allow-read, --allow-net, etc.), explicit file extensions in imports, Deno KV operations, Deno Deploy patterns, deno.json workspace config, Deno.test patterns, Fresh island architecture, and web-standard API usage over Node.js polyfills.

Rules Configuration

---
description: Enforces Deno best practices including permission flags, ESM imports with extensions, Deno KV, Deno Deploy, deno.json configuration, Deno.test patterns, WebSocket usage, and web-standard APIs. Provides guidelines for Fresh framework and secure runtime development.
globs: **/*.ts,**/*.tsx,**/*.js,**/*.jsx,deno.json,deno.jsonc,fresh.config.ts
---
# Deno Best Practices

You are an expert in Deno, modern JavaScript/TypeScript runtimes, and secure server-side development.
You understand permission models, ESM module resolution, Deno KV, Deno Deploy, Deno's standard library, and the Fresh web framework.

### Module Imports
- Always include the file extension in local imports: `import { foo } from "./foo.ts"`
- Use `jsr:` prefix for JSR packages: `import { serve } from "jsr:@std/http"`
- Use `npm:` prefix for npm packages: `import chalk from "npm:chalk"`
- Never use bare specifiers without a prefix (no `import express from "express"`)
- Use import maps in deno.json for path aliases and dependency management
- Remote imports must use full URLs with version pinning

### Permissions
- Never request blanket permissions (--allow-all). Use granular flags: --allow-read, --allow-net, --allow-env, --allow-write
- Scope permissions to specific paths: --allow-read=/app/data
- Use Deno.permissions API to query and revoke permissions at runtime
- Document required permissions in README or deno.json tasks
- Use --deny-net/--deny-read to explicitly block sensitive paths

### Configuration (deno.json)
- Use deno.json (or deno.jsonc) as the single project config file, not tsconfig.json
- Define tasks in the `tasks` field: "dev": "deno run --watch main.ts"
- Use `imports` map for dependency aliases and version management
- Set `compilerOptions` for strict TypeScript (Deno defaults to strict)
- Configure `lint.rules` and `fmt.options` for code style enforcement
- Use `workspaces` for monorepo setups

### Runtime APIs
- Use web-standard APIs: fetch, Request, Response, WebSocket, ReadableStream, URL
- Access environment variables via Deno.env.get("KEY"), never process.env
- Use Deno.readFile/Deno.writeFile for file system operations (with permissions)
- Use Deno.serve() for HTTP servers, not Node.js http module
- Prefer top-level await over async wrapper functions
- Handle errors with try/catch and standard Deno error types

### Deno KV
- Use Deno.openKv() for a local key-value store (SQLite-backed)
- Connect to remote KV: Deno.openKv("https://api.deno.com/kv/...")
- Store values with `await kv.set(["users", userId], userData)`
- Retrieve with `await kv.get(["users", userId])`
- List with prefix: `await Array.fromAsync(kv.list({ prefix: ["users"] }))`
- Use `kv.atomic()` for transactions across multiple keys
- Set expiration: `await kv.set(["cache", key], value, { expireIn: 60000 })`
- Close KV in teardown: `kv.close()`

### WebSocket
- Use `Deno.upgradeWebSocket(request)` to upgrade HTTP to WebSocket
- Handle events: `socket.onopen`, `socket.onmessage`, `socket.onclose`, `socket.onerror`
- Send messages: `socket.send(JSON.stringify(data))`
- Use WebSocketStream API for streaming: `const stream = new WebSocketStream(url)`
- Broadcast to multiple clients by maintaining a Set of sockets

### Deno Deploy
- Serverless platform with automatic scaling at the edge
- Cold start times under 100ms — no long-lived connections
- Use `Deno.serve()` entry point for the handler function
- Environment variables set via Deploy dashboard, accessed with Deno.env.get()
- No file system access — use KV or external storage instead
- Cron jobs via Deno.cron(): `Deno.cron("job-name", "0 0 * * *", handler)`
- Deploy via GitHub integration or `deployctl deploy`

### Testing (Deno.test)
- Use Deno.test() with a descriptive name string as the first argument
- Group related tests with Deno.test({ name, fn }) combined with t.step()
- Use t.step() for sub-tests within a test function
- Import assert, assertEquals, assertThrows from "jsr:@std/assert"
- Use fake timers with FakeTime from "@std/testing/time"
- Snapshot testing: import { assertSnapshot } from "@std/testing/snapshot"
- Run with deno test --allow-read --allow-env to match test permission needs

### Fresh Framework
- Routes live in `routes/` directory — file names become URL paths
- Use Handlers (GET/POST) and components in the same route file
- Islands (interactive components) live in `islands/` directory
- Prefer islands over global JavaScript for interactivity
- Use `FreshContext` and route params via ctx.params, ctx.url
- Static files go in `static/` directory, served at root URL
- Use Preact with hooks for component logic (React-compatible)
- app.ts configures the Fresh app: export default new FreshApp(config)

### Formatting & Linting
- Run `deno fmt` for consistent formatting (no Prettier needed)
- Run `deno lint` for static analysis with built-in rules
- Run `deno check` for TypeScript type checking without execution
- Use `// deno-lint-ignore` to suppress specific lint rules
- Configure `lint.rules.tags: ["recommended"]` in deno.json

Installation

Create deno.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.

# Initialize a Deno project
deno init my-project

# Run with permissions
deno run --allow-read --allow-net main.ts

# Run tests
deno test --allow-read --allow-env

Examples

// main.ts — HTTP server with Deno.serve (built-in, no import needed)
Deno.serve({ port: 3000 }, async (req: Request): Promise<Response> => {
  const url = new URL(req.url);

  if (url.pathname === "/api/health") {
    return new Response(JSON.stringify({ status: "ok" }), {
      headers: { "content-type": "application/json" },
    });
  }

  return new Response("Not Found", { status: 404 });
});
// kv.ts — Deno KV usage with transactions
const kv = await Deno.openKv();

// Store user
await kv.set(["users", "alice"], {
  name: "Alice",
  email: "[email protected]",
});

// Atomic transaction
const result = await kv.atomic()
  .check({ key: ["users", "alice"], versionstamp: null }) // create if not exists
  .set(["users", "alice"], { name: "Alice", email: "[email protected]", joined: Date.now() })
  .commit();

kv.close();
// routes/api/users.ts — Fresh API route
import { FreshContext } from "$fresh/server.ts";

interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

export const handler = async (
  _req: Request,
  ctx: FreshContext,
): Promise<Response> => {
  return new Response(JSON.stringify(users), {
    headers: { "content-type": "application/json" },
  });
};
// deno_test.ts — Testing with Deno.test and std/assert
import { assertEquals, assertThrows } from "jsr:@std/assert";

function divide(a: number, b: number): number {
  if (b === 0) throw new Error("Division by zero");
  return a / b;
}

Deno.test("divide — basic division", () => {
  assertEquals(divide(10, 2), 5);
});

Deno.test("divide — division by zero", () => {
  assertThrows(() => divide(10, 0), Error, "Division by zero");
});

Deno.test("divide — step-based grouping", async (t) => {
  await t.step("positive numbers", () => {
    assertEquals(divide(6, 3), 2);
  });

  await t.step("negative numbers", () => {
    assertEquals(divide(-6, 3), -2);
  });
});