Deno: A Secure Runtime for JavaScript and TypeScript
A junior developer on our team ran npm install on a package that, among other things, read environment variables and POSTed them to an external server. Supply chain attacks aren’t theoretical. Node.js runs with full system access by default—file system, network, environment variables, subprocess execution. Every npm install is an act of trust.
Ryan Dahl (who created Node.js) built Deno to address what he saw as Node’s design mistakes: security as opt-in rather than default, node_modules complexity, and CommonJS/ESM friction. Deno is provocative. It’s also genuinely useful for specific workloads.
I wouldn’t rewrite our production API in Deno tomorrow. But for CLI tools, scripts, and edge functions where security and TypeScript matter? It’s become my default.
What Makes Deno Different
| Feature | Deno | Node.js |
|---|---|---|
| Security | Explicit permissions | Full access by default |
| TypeScript | Built-in, no config | Requires setup |
| Modules | URL imports, no node_modules | npm + node_modules |
| Standard library | First-party, reviewed | Community packages |
| Tooling | fmt, lint, test, bundle built-in | Ecosystem tools |
Deno isn’t “Node but newer.” It’s a different philosophy: secure by default, batteries included, web standards first.
Getting Started
# Install
curl -fsSL https://deno.land/install.sh | sh
# Or: brew install deno
deno --version
No package.json. No node_modules. No tsconfig.json. Write TypeScript, run it:
// hello.ts
console.log('Hello, Deno!');
deno run hello.ts
The simplicity is refreshing after years of JavaScript toolchain archaeology.
Permissions: Security by Default
This is Deno’s killer feature. By default, Deno can do nothing—no file access, no network, no environment variables, no subprocesses.
# Network only
deno run --allow-net server.ts
# File read + write
deno run --allow-read=./data --allow-write=./output script.ts
# Environment variables (specific)
deno run --allow-env=DATABASE_URL app.ts
# Everything (escape hatch)
deno run --allow-all script.ts # Use sparingly
When you run without permissions, Deno tells you exactly what flag to add:
error: Requires net access to "api.example.com:443"
For production scripts and CLIs distributed to users, this matters. Your tool can’t secretly phone home without the user explicitly granting network access.
Runtime Permission Requests
const status = await Deno.permissions.request({ name: "net", host: "api.example.com" });
if (status.state === "granted") {
const response = await fetch("https://api.example.com/data");
}
Interactive permission prompts for user-facing applications.
HTTP Server: Web Standards
Deno uses web platform APIs—fetch, Request, Response, WebSocket:
Deno.serve({ port: 8000 }, (req) => {
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 });
});
deno run --allow-net server.ts
No Express import. No framework required for simple APIs. The same fetch API you use in browsers works in Deno—skills transfer.
Standard Library: Curated, Not Crowdsourced
// File operations
const text = await Deno.readTextFile("./config.json");
await Deno.writeTextFile("./output.txt", "result");
// Directory iteration
for await (const entry of Deno.readDir("./src")) {
console.log(entry.name);
}
// HTTP server (older std pattern — Deno.serve preferred now)
import { serve } from "https://deno.land/std@0.200.0/http/server.ts";
Deno’s standard library is reviewed by the core team. No guessing if a package is maintained or compromised—at least for std.
Module System: URLs, Not node_modules
// Import from URL — version pinned
import { serve } from "https://deno.land/std@0.200.0/http/server.ts";
// Import local files (TypeScript natively)
import { validateEmail } from "./utils/validation.ts";
No npm install. Dependencies are URLs, cached globally on your machine. deno cache main.ts pre-downloads everything.
Import maps for cleaner imports:
{
"imports": {
"std/": "https://deno.land/std@0.200.0/",
"@utils/": "./src/utils/"
}
}
import { serve } from "std/http/server.ts";
import { validateEmail } from "@utils/validation.ts";
deno run --import-map=import_map.json app.ts
npm compatibility arrived in Deno 1.28+ (deno install npm:express). You can use npm packages when needed, but the Deno-native approach is URL imports.
Built-in Tooling
deno fmt # Format code
deno lint # Lint code
deno test # Run tests
deno check app.ts # Type-check without running
deno compile # Build standalone executable
No Prettier, ESLint, Jest, or Webpack configuration. One toolchain.
Testing
import { assertEquals, assertExists } from "https://deno.land/std@0.200.0/testing/asserts.ts";
Deno.test("adds numbers correctly", () => {
assertEquals(1 + 1, 2);
});
Deno.test("fetches user data", async () => {
const response = await fetch("https://api.example.com/users/1");
assertEquals(response.status, 200);
const user = await response.json();
assertExists(user.name);
});
deno test --allow-net
When I Reach for Deno
Good fit:
- CLI tools and scripts (especially distributed to others)
- Internal tools where security matters
- Prototyping APIs quickly
- Edge functions (Deno Deploy)
- Teaching JavaScript/TypeScript (less toolchain friction)
- Greenfield projects wanting TypeScript from line one
Stick with Node.js:
- Large existing Node codebases
- npm ecosystem dependencies you can’t live without
- Teams deeply invested in Node tooling
- Libraries targeting maximum distribution
Deno Deploy: Edge Runtime
Deno’s hosted platform runs Deno at the edge globally:
// Deploy to Deno Deploy — no config needed
Deno.serve(async (req) => {
const data = await fetch("https://api.example.com/data");
return new Response(data.body, {
headers: { "content-type": "application/json" },
});
});
Git push → global deployment. Free tier for side projects. Competitive with Cloudflare Workers for simple edge APIs.
Practical Tips
- Start restrictive on permissions — add only what you need
- Pin dependency versions in URLs —
std@0.200.0, notstd@latest - Use
deno.jsonfor project config (tasks, imports, lint rules) - Compile standalone binaries —
deno compile --allow-net -o mycli main.ts - Leverage web APIs —
fetch,crypto,WebSocketwork identically to browsers - Check npm compatibility — many packages work; some don’t (native addons)
Conclusion
Deno won’t replace Node.js in most production environments tomorrow. The npm ecosystem’s gravity is immense. But Deno’s ideas—secure by default, TypeScript native, web standard APIs, integrated tooling—are influencing the entire JavaScript runtime space.
For scripts that touch sensitive data, CLI tools you distribute, quick APIs, and edge functions, Deno is my first choice. The permission model alone justifies it: code can’t access the network or file system without explicit user consent.
Node taught the world JavaScript on the server. Deno asks: what if we designed it differently knowing what we know now? Whether you adopt Deno or not, that question produces better tools everywhere.
Try it for your next script. You’ll miss node_modules exactly zero times.
Deno secure runtime from March 2021, covering permissions, standard library, and modern JavaScript features.