languages
May 15, 2026 · 8 min read · 0 views

Deno 2.1: NPM Workspaces, JSR Enhancements, and Native Type Stripping

Deno 2.1 brings monorepo support via NPM workspaces, improved JSR publishing, and native TypeScript type stripping. Learn what's new and how to migrate.

Overview

Deno 2.1 represents a significant step forward in making Deno the preferred runtime for modern JavaScript and TypeScript development. Released in late 2024, this update focuses on improving the developer experience for teams managing large codebases, enhancing the JSR (Jsr.io) ecosystem, and streamlining TypeScript-to-JavaScript workflows. Unlike previous releases that emphasized compatibility, Deno 2.1 doubles down on what makes Deno unique: secure-by-default design, built-in tooling, and first-class TypeScript support.

Key Features in Deno 2.1

1. NPM Workspaces Support

Deno now fully supports NPM workspaces, allowing developers to manage monorepos with multiple interconnected packages. This is crucial for teams migrating from Node.js or managing complex projects with shared dependencies.

Before (workaround): You’d have to manually symlink packages or use complex build systems:

# Old approach: separate deno.json files per package
cd packages/api && deno run src/server.ts
cd ../web && deno run src/app.ts

After (Deno 2.1): Define workspaces in your root deno.json:

{
  "workspace": [
    "./packages/api",
    "./packages/web",
    "./packages/shared"
  ]
}

Each package gets its own deno.json with local dependencies:

// packages/api/deno.json
{
  "name": "@myapp/api",
  "version": "1.0.0",
  "imports": {
    "@myapp/shared": "../shared/mod.ts"
  },
  "exports": {
    ".": "./src/server.ts"
  }
}

Now run the entire workspace:

deno run --allow-net packages/api/src/server.ts

Deno automatically resolves cross-package imports, eliminating build steps and keeping TypeScript types intact across packages.

2. JSR Enhancements

The JSR (JavaScript Registry) ecosystem expanded significantly in Deno 2.1. JSR is an npm alternative built for TypeScript-first development—packages ship with types by default, no separate @types packages needed.

Publishing to JSR is now streamlined:

deno publish

Deno scans your deno.json for package metadata and publishes directly to JSR:

{
  "name": "@my-org/logger",
  "version": "2.0.1",
  "exports": {
    ".": "./src/mod.ts",
    "./debug": "./src/debug.ts"
  }
}

Consumers install from JSR with zero type friction:

deno add @my-org/logger

This generates an import in deno.json:

{
  "imports": {
    "@my-org/logger": "jsr:@my-org/logger@^2.0"
  }
}

No package-lock.json, no missing types, no npm install required. JSR packages work seamlessly across Deno, Node.js (via npm or yarn), and browsers.

3. Native TypeScript Type Stripping

Deno 2.1 introduces native TypeScript-to-JavaScript stripping without requiring a bundler or external transpiler. This means your TypeScript source code runs directly in production environments that don’t support TypeScript syntax.

Example workflow:

// src/api.ts
interface User {
  id: string;
  name: string;
}

export function getUserName(user: User): string {
  return user.name;
}

Previously, you’d need esbuild or similar to strip types before deployment. Now:

deno bundle --allow-all src/api.ts dist/api.js

Generated dist/api.js:

export function getUserName(user) {
  return user.name;
}

Types are cleanly removed, and the runtime adds only ~2KB overhead. This is ideal for serverless functions, edge computing, or any environment where TypeScript isn’t available.

Getting Started with Deno 2.1

Installation

If you haven’t used Deno before, install it:

# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows (PowerShell)
iwr https://deno.land/install.ps1 -useb | iex

# Verify
deno --version

Create Your First Workspace

mkdir my-deno-monorepo && cd my-deno-monorepo

# Initialize root workspace
echo '{
  "workspace": ["./packages/core", "./packages/web"]
}' > deno.json

mkdir -p packages/core packages/web

Set Up a Shared Package

# packages/core/deno.json
cat > packages/core/deno.json << 'EOF'
{
  "name": "@myapp/core",
  "version": "1.0.0",
  "exports": {
    ".": "./mod.ts"
  }
}
EOF

# packages/core/mod.ts
cat > packages/core/mod.ts << 'EOF'
export function add(a: number, b: number): number {
  return a + b;
}
EOF

Use It in Another Package

# packages/web/deno.json
cat > packages/web/deno.json << 'EOF'
{
  "name": "@myapp/web",
  "version": "1.0.0",
  "imports": {
    "@myapp/core": "../core/mod.ts"
  }
}
EOF

# packages/web/app.ts
cat > packages/web/app.ts << 'EOF'
import { add } from "@myapp/core";

console.log("2 + 3 =", add(2, 3));
EOF

Run it:

deno run packages/web/app.ts
# Output: 2 + 3 = 5

Practical Use Cases

Use Case 1: Publishing a Utility Library to JSR

You’ve built a useful TypeScript utility. With Deno 2.1, publishing is trivial:

cat > deno.json << 'EOF'
{
  "name": "@your-org/parse-query",
  "version": "1.0.0",
  "exports": {
    ".": "./src/mod.ts"
  }
}
EOF

cat > src/mod.ts << 'EOF'
export function parseQuery(qs: string): Record<string, string> {
  const params = new URLSearchParams(qs);
  return Object.fromEntries(params);
}
EOF

deno publish

Consumers get full TypeScript support immediately:

import { parseQuery } from "jsr:@your-org/parse-query";

const params = parseQuery("name=alice&age=30");
// types inferred without @types package

Use Case 2: Type-Safe Serverless Functions

Use Deno 2.1’s type stripping for edge deployments (Cloudflare Workers, Deno Deploy, etc.):

// handler.ts
interface Request {
  user: { id: string; role: "admin" | "user" };
  action: string;
}

export function authorize(req: Request): boolean {
  return req.user.role === "admin";
}

Strip and deploy:

deno bundle --allow-all handler.ts handler.js
# Upload handler.js to your edge platform

Types are removed but your logic is preserved, and the bundle is minimal.

Step-by-Step Migration Guide

From Node.js + npm to Deno 2.1

Step 1: Initialize Deno

cd my-node-project
deno init
# Creates deno.json with empty imports

Step 2: Port package.json dependencies

If your package.json has:

{
  "dependencies": {
    "express": "^4.18.0",
    "dotenv": "^16.0.0"
  }
}

Add equivalents to deno.json:

{
  "imports": {
    "express": "npm:express@^4.18.0",
    "dotenv": "npm:dotenv@^16.0.0"
  }
}

Or use JSR packages if available:

{
  "imports": {
    "std/env": "jsr:@std/env@^0.200.0"
  }
}

Step 3: Update imports in your code

// Before (Node.js)
import express from "express";
import dotenv from "dotenv";

// After (Deno)
import express from "express";
import { load } from "dotenv";

Step 4: Remove node_modules, package-lock.json

rm -rf node_modules package-lock.json

Deno manages dependencies globally in ~/.deno—no disk bloat.

Step 5: Run your app

deno run --allow-net --allow-env src/server.ts

Common Pitfalls and Solutions

Pitfall 1: Import Path Confusion

Problem: Mixing JSR, npm, and local imports feels chaotic.

Solution: Be explicit with prefixes:

// Local file
import { helper } from "./helpers.ts";

// JSR package
import { parse } from "jsr:@std/json";

// npm package
import express from "npm:express";

Use the JSON Formatter to validate your deno.json imports structure.

Pitfall 2: Type-Only Imports in Bundling

Problem: When bundling, type imports might cause confusion.

Solution: Use TypeScript’s type keyword:

import type { User } from "./types.ts";
import { processUser } from "./processor.ts";

When Deno strips types, type imports are completely removed, keeping bundle size minimal.

Pitfall 3: Workspace Package Version Mismatches

Problem: Monorepo packages have conflicting versions.

Solution: Use Deno’s workspace resolution to lock versions. In your root deno.json:

{
  "imports": {
    "shared-utils": "./packages/shared/mod.ts"
  },
  "workspace": ["./packages/api", "./packages/web"]
}

All packages now share the same version of shared-utils automatically.

Pitfall 4: JSR Publishing Metadata

Problem: Forgetting required fields in deno.json causes publish failures.

Solution: Validate your deno.json structure with the JSON Formatter:

{
  "name": "@my-org/package",
  "version": "1.0.0",
  "description": "Brief description",
  "license": "MIT",
  "exports": {
    ".": "./src/mod.ts"
  }
}

All fields except exports are optional, but recommended for discoverability on jsr.io.

Why It Matters

Deno 2.1 closes the gap between development and production TypeScript. Historically, TypeScript developers faced friction:

  1. Monorepo complexity: Tools like Lerna and Yarn workspaces required intricate configuration.
  2. Type ecosystem fragmentation: npm’s @types/* packages created maintenance overhead and sometimes went stale.
  3. Transpilation overhead: Every build required a separate bundler pass.

Deno 2.1 eliminates these pain points:

  • Native workspace support means monorepos work out-of-the-box.
  • JSR ecosystem ensures types are always current and included.
  • Type stripping lets you use TypeScript everywhere without transpiler dependency.

For teams tired of Node.js ecosystem complexity, Deno 2.1 offers a fresh, cohesive alternative. For TypeScript-first organizations, it’s now a serious contender for production workloads.

Advanced: Building a Full-Stack Deno App

Let’s create a realistic monorepo: API server + web frontend + shared utilities.

Directory structure:

my-app/
├── deno.json
├── packages/
│   ├── api/
│   │   ├── deno.json
│   │   └── src/
│   │       └── server.ts
│   ├── web/
│   │   ├── deno.json
│   │   └── src/
│   │       └── app.tsx
│   └── types/
│       ├── deno.json
│       └── mod.ts

Root deno.json:

{
  "workspace": ["./packages/api", "./packages/web", "./packages/types"]
}

packages/types/mod.ts:

export interface User {
  id: string;
  email: string;
  role: "admin" | "user";
}

export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

packages/api/deno.json:

{
  "name": "@app/api",
  "version": "1.0.0",
  "imports": {
    "@app/types": "../types/mod.ts",
    "std/http": "jsr:@std/http@^0.200.0"
  }
}

packages/api/src/server.ts:

import { serve } from "std/http";
import type { User, ApiResponse } from "@app/types";

const handler = (req: Request): Response => {
  const response: ApiResponse<User> = {
    success: true,
    data: { id: "1", email: "[email protected]", role: "user" }
  };
  return new Response(JSON.stringify(response), {
    headers: { "Content-Type": "application/json" }
  });
};

serve(handler, { port: 3000 });
console.log("Server running on http://localhost:3000");

packages/web/deno.json:

{
  "name": "@app/web",
  "version": "1.0.0",
  "imports": {
    "@app/types": "../types/mod.ts",
    "preact": "npm:preact@^10.20.0"
  }
}

packages/web/src/app.tsx:

import { render } from "preact";
import type { User } from "@app/types";

const UserCard = ({ user }: { user: User }) => (
  <div className="card">
    <h2>{user.email}</h2>
    <p>Role: {user.role}</p>
  </div>
);

const app = () => <UserCard user={{ id: "1", email: "[email protected]", role: "user" }} />;
render(app(), document.body);

Run the API:

deno run --allow-net packages/api/src/server.ts

Build the web app:

deno bundle --allow-all packages/web/src/app.tsx dist/app.js

Both share type definitions with zero duplication. That’s the power of Deno 2.1 workspaces.

Conclusion

Deno 2.1 represents a maturation of the Deno runtime. The addition of NPM workspaces, JSR ecosystem improvements, and native type stripping removes major friction points for TypeScript development. Whether you’re managing a large monorepo, publishing utilities, or deploying serverless functions, Deno 2.1 provides the tooling to do it efficiently.

The migration path from Node.js is clear, the ecosystem is growing, and the developer experience is genuinely compelling. If you’ve dismissed Deno in the past, 2.1 is worth revisiting.

This post was generated with AI assistance and reviewed for accuracy.