Claude Code with Deno and Bun: Modern JavaScript Runtimes — Claude Skills 360 Blog
Blog / Development / Claude Code with Deno and Bun: Modern JavaScript Runtimes
Development

Claude Code with Deno and Bun: Modern JavaScript Runtimes

Published: June 18, 2026
Read time: 9 min read
By: Claude Skills 360

Deno and Bun represent different bets on what’s wrong with Node.js. Deno bets on security, standards compliance, and first-class TypeScript. Bun bets on speed — faster startup, faster npm install, faster tests. Both work well with Claude Code, which understands their module systems, permission models, and standard library differences from Node.

This guide covers using Claude Code with Deno 2 and Bun: setup patterns, migration from Node.js, the differences that matter, and where each runtime excels.

Deno 2

CLAUDE.md for Deno Projects

## Deno 2 Project

- Runtime: Deno 2.x
- Package registry: JSR (jsr:) preferred, npm: for Node packages
- No package.json — use deno.json for config and imports
- TypeScript: native, no compilation step
- Permissions: explicit flags required (--allow-net, --allow-read, etc.)
- Test runner: `deno test` (built-in)
- Formatter: `deno fmt`
- Linter: `deno lint`
- Standard library: jsr:@std/* (not npm std packages)

## Import conventions
- JSR packages: `import { ... } from "jsr:@std/http"`
- npm packages: `import express from "npm:express"`
- Local: `import { ... } from "./lib/utils.ts"` (always with extension)
- Do NOT use bare imports without specifiers

Deno Configuration

// deno.json
{
  "tasks": {
    "dev": "deno run --allow-net --allow-read --allow-env --watch src/main.ts",
    "test": "deno test --allow-net --allow-read --allow-env",
    "lint": "deno lint",
    "fmt": "deno fmt"
  },
  "imports": {
    "@std/http": "jsr:@std/http@^1.0",
    "@std/path": "jsr:@std/path@^1.0",
    "@std/assert": "jsr:@std/assert@^1.0",
    "hono": "jsr:@hono/hono@^4.0"
  },
  "compilerOptions": {
    "strict": true
  },
  "lint": {
    "rules": {
      "tags": ["recommended"]
    }
  },
  "fmt": {
    "lineWidth": 100,
    "singleQuote": true
  }
}

HTTP Server with Hono

Build a REST API with Deno 2 and Hono.
User CRUD with JWT auth and Deno KV for storage.
// src/main.ts
import { Hono } from 'hono';
import { jwt } from 'hono/jwt';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { usersRouter } from './routes/users.ts';

const app = new Hono();

app.use('*', logger());
app.use('*', cors({ origin: Deno.env.get('ALLOWED_ORIGIN') ?? '*' }));

// JWT middleware for protected routes
app.use('/api/*', jwt({ secret: Deno.env.get('JWT_SECRET') ?? 'dev-secret' }));

app.route('/api/users', usersRouter);

app.get('/health', (c) => c.json({ ok: true }));

Deno.serve({ port: parseInt(Deno.env.get('PORT') ?? '8000') }, app.fetch);
// src/routes/users.ts
import { Hono } from 'hono';
import { z } from 'npm:zod';
import { zValidator } from 'npm:@hono/zod-validator';

const kv = await Deno.openKv();
const router = new Hono();

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

router.get('/', async (c) => {
  const users: User[] = [];
  for await (const entry of kv.list<User>({ prefix: ['users'] })) {
    users.push(entry.value);
  }
  return c.json({ users });
});

router.post('/', zValidator('json', createUserSchema), async (c) => {
  const data = c.req.valid('json');
  const id = crypto.randomUUID();
  const user: User = { id, ...data, createdAt: new Date().toISOString() };
  
  // Atomic: check uniqueness, then insert
  const result = await kv.atomic()
    .check({ key: ['users_by_email', data.email], versionstamp: null }) // Must not exist
    .set(['users', id], user)
    .set(['users_by_email', data.email], id)
    .commit();
  
  if (!result.ok) {
    return c.json({ error: 'Email already exists' }, 409);
  }
  
  return c.json({ user }, 201);
});

router.get('/:id', async (c) => {
  const entry = await kv.get<User>(['users', c.req.param('id')]);
  if (!entry.value) return c.json({ error: 'Not found' }, 404);
  return c.json({ user: entry.value });
});

export { router as usersRouter };

Deno Testing

Write tests for the users API.
Use Deno's built-in test runner without external testing libraries.
// src/routes/users_test.ts
import { assertEquals, assertExists } from '@std/assert';
import app from '../main.ts';

// Deno's built-in test runner
Deno.test('GET /health returns 200', async () => {
  const req = new Request('http://localhost/health');
  const res = await app.fetch(req);
  assertEquals(res.status, 200);
  const body = await res.json();
  assertEquals(body.ok, true);
});

Deno.test('POST /api/users creates a user', async () => {
  const req = new Request('http://localhost/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${await generateTestToken()}`,
    },
    body: JSON.stringify({ email: '[email protected]', name: 'Test User' }),
  });
  
  const res = await app.fetch(req);
  assertEquals(res.status, 201);
  
  const body = await res.json();
  assertExists(body.user.id);
  assertEquals(body.user.email, '[email protected]');
});

Deno.test({
  name: 'POST /api/users rejects duplicate email',
  async fn() {
    // Create first
    await createUser({ email: '[email protected]', name: 'First' });
    
    // Try to create duplicate
    const res = await postUser({ email: '[email protected]', name: 'Second' });
    assertEquals(res.status, 409);
  },
  sanitizeResources: false, // Required when using Deno KV in tests
});
deno test --allow-net --allow-read --allow-env --allow-write

Permissions Model

Our Deno app needs to read config files, make HTTP requests to the payment API,
and write to /tmp. Set up minimal permissions.

Claude Code generates the minimal permission set:

# Development — permissive
deno run --allow-net --allow-read --allow-env --allow-write=/tmp src/main.ts

# Production — locked down
deno run \
  --allow-net=api.stripe.com,api.sendgrid.com \  # Only specific hosts
  --allow-read=./config/,./public/ \              # Only config and public dirs
  --allow-env=DATABASE_URL,STRIPE_KEY,PORT \      # Only named env vars
  --allow-write=/tmp \                            # Only tmp for uploads
  --no-prompt \                                   # Never prompt for permission at runtime
  src/main.ts

Deno’s permission model forces you to declare all I/O at startup — which Claude Code uses to surface security issues. “Your code reads files directly from user input — that’s a path traversal risk with --allow-read.”

Bun

CLAUDE.md for Bun Projects

## Bun Project

- Runtime: Bun 1.x
- Package manager: bun (not npm/yarn)
- TypeScript: native, no compilation step
- Test runner: `bun test` (Jest-compatible API)
- Bundler: `bun build` (replaces webpack/esbuild)
- Hot reload: `bun --hot src/index.ts`
- node_modules: uses npm registry, bun.lockb lock file

## Commands
- Install: `bun install`
- Run: `bun run src/index.ts`
- Test: `bun test`
- Build: `bun build src/index.ts --outdir dist/`

Bun HTTP Server

Build an API server with Bun's native HTTP server.
No frameworks — just Bun's built-in Bun.serve().
// src/index.ts
const server = Bun.serve({
  port: parseInt(process.env.PORT ?? '3000'),
  
  async fetch(req: Request): Promise<Response> {
    const url = new URL(req.url);
    
    // Router
    if (url.pathname === '/health' && req.method === 'GET') {
      return Response.json({ ok: true });
    }
    
    if (url.pathname.startsWith('/api/')) {
      return handleApi(req, url);
    }
    
    return new Response('Not found', { status: 404 });
  },
  
  error(err: Error): Response {
    console.error(err);
    return Response.json({ error: 'Internal server error' }, { status: 500 });
  },
});

console.log(`Listening on ${server.url}`);

async function handleApi(req: Request, url: URL): Promise<Response> {
  // Auth
  const token = req.headers.get('Authorization')?.replace('Bearer ', '');
  if (!token || !await verifyJWT(token)) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  const [, , resource, id] = url.pathname.split('/');
  
  if (resource === 'users') {
    return handleUsers(req, id);
  }
  
  return Response.json({ error: 'Not found' }, { status: 404 });
}

Bun’s Test Runner

// src/index.test.ts
import { expect, test, describe, beforeAll, afterAll } from 'bun:test';

let server: ReturnType<typeof Bun.serve>;

beforeAll(() => {
  server = Bun.serve({ port: 0, fetch: app.fetch }); // port: 0 = random available port
});

afterAll(() => server.stop(true));

describe('Users API', () => {
  test('GET /health returns 200', async () => {
    const res = await fetch(`${server.url}health`);
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ ok: true });
  });
  
  test('POST /api/users creates user', async () => {
    const res = await fetch(`${server.url}api/users`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${testToken}`,
      },
      body: JSON.stringify({ email: '[email protected]', name: 'Bun User' }),
    });
    
    expect(res.status).toBe(201);
    const { user } = await res.json();
    expect(user.email).toBe('[email protected]');
  });
});
bun test --watch        # watch mode
bun test --coverage     # coverage report
bun test src/api/       # run specific directory

Bun as Drop-in npm Replace

Our Node.js project has slow `npm install` in CI (45 seconds).
Migrate to Bun for package management while keeping Node.js as runtime.
# Remove existing lock files
rm package-lock.json yarn.lock

# Install Bun
curl -fsSL https://bun.sh/install | bash

# Install packages (3-5x faster than npm)
bun install

# This generates bun.lockb — commit it
git add bun.lockb package.json
# .github/workflows/ci.yml — replace npm steps
- name: Setup Bun
  uses: oven-sh/setup-bun@v1
  with:
    bun-version: latest

- name: Install dependencies
  run: bun install --frozen-lockfile  # Equivalent of npm ci

- name: Run tests
  run: bun test  # Or: bun run test (uses package.json script)

Bun’s install reads from the same package.json and node_modules. Your Node.js code runs unchanged — only the package manager (and optionally the test runner) changes.

Migrating from Node.js

Migrate this Node.js Express app to Deno 2.
The app uses fs, path, and crypto from Node's stdlib.

Claude Code handles the migration systematically:

// Node.js original
import { readFileSync } from 'fs';
import { join } from 'path';
import { createHash } from 'crypto';

const config = JSON.parse(readFileSync(join(__dirname, 'config.json'), 'utf-8'));
const hash = createHash('sha256').update(data).digest('hex');

// Deno equivalent
const config = JSON.parse(
  await Deno.readTextFile(new URL('./config.json', import.meta.url))
);
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
const hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');

Common patterns Claude Code knows to translate:

  • __dirnamenew URL('.', import.meta.url).pathname
  • process.env.XDeno.env.get('X') (Deno) or process.env.X (Bun, compatible)
  • fs.readFileDeno.readTextFile / Bun.file().text()
  • require()import (both Deno and Bun support ESM only)
  • node:cryptocrypto.subtle (both runtimes support Web Crypto API natively)

For TypeScript setup that works across Node.js, Deno, and Bun, see the TypeScript guide. For deploying Deno applications to the edge, see the serverless guide. The Claude Skills 360 bundle includes runtime-specific skill sets for Deno and Bun migrations. Start with the free tier to try runtime migration prompts.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free