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:
- Monorepo complexity: Tools like Lerna and Yarn workspaces required intricate configuration.
-
Type ecosystem fragmentation: npm’s
@types/*packages created maintenance overhead and sometimes went stale. - 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.