Right, so you’ve escaped the browser. Good for you. The first thing you’ll notice when you fire up Deno is the sheer, unadulterated relief of not having to configure a tsconfig.json file that’s 80 lines long just to get strict mode working. Deno’s creators, in a moment of clarity, decided that TypeScript is the runtime. It’s not an add-on; it’s the default. This means you can write a .ts file and just… run it. No tsc, no ts-node, no nodemon watching a dist/ directory. It feels like magic, but it’s just good design.

Here’s the canonical “hello world” that will make any Node.js developer do a double-take:

// hello.ts
const greeting: string = "Hello, Deno!";
console.log(greeting);

Now, to run it:

deno run hello.ts

And that’s it. No build step. Deno compiles it to JavaScript in memory, caches the result, and executes it. The first time might have a tiny hiccup as it compiles, but subsequent runs are blazingly fast thanks to that cache. It’s the developer experience we were promised but never got.

How Deno Pulls This Off (The Magic Trick Revealed)

Deno doesn’t use the official TypeScript compiler (tsc). Instead, it uses a Rust-based TypeScript compiler that leverages the swc project. This is the secret sauce. swc is an order of magnitude faster than tsc because it’s written in a systems language, and it’s what allows Deno to compile your code on the fly without you waiting for a coffee break. It consumes your TypeScript code and your import statements and handles everything in one go. This also means Deno has a single, unified module resolution system. Forget node_modules; it’s a cursed artifact we’re all trying to forget.

The Module System: Bye-Bye, node_modules

Speaking of which, Deno uses URLs for imports. Yes, it looks weird at first. No, it doesn’t bite.

// Importing from a URL? Absolutely.
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";

// Importing a local module? Just like any other relative path.
import { localUtility } from "./lib/utils.ts";

serve(async (req) => {
  return new Response(`You asked for ${req.url}`);
});

This is Deno’s most opinionated—and frankly, brilliant—choice. Your dependencies are explicitly declared right in the code. No more guessing which version of lodash is mysteriously installed 17 directories deep in node_modules. Deno downloads, caches, and versions these modules for you. Run the file, and it’ll pull down anything it doesn’t have locally. For production, you can create a deps.ts file to centralize your external imports, which is a solid practice.

Permissions: The Security Model You Didn’t Know You Needed

Node lets your code do anything. Deno thinks that’s a terrible idea. By default, a Deno script has no permissions. It can’t access the network, the filesystem, or the environment. You have to explicitly grant that power. This seems annoying until you realize it prevents a rogue dependency from emailing your SSH keys to a remote server.

Trying to run our server above with just deno run server.ts will fail spectacularly. It needs network access.

# This will work. The --allow-net flag grants it permission.
deno run --allow-net server.ts

# Want to be more specific? Fantastic. Only allow access to specific hosts.
deno run --allow-net=github.com,api.stripe.com server.ts

This permissions model is a killer feature. It forces you to think about what your code actually needs and prevents entire classes of supply-chain attacks. It’s a bit of a pain to type, but your security posture will thank you.

The Rough Edges and Pitfalls

It’s not all sunshine and roses. The biggest “gotcha” is that Deno aims for browser compatibility, not Node compatibility. This means the built-in modules you’re used to—fs, path, http—are different. They follow web standards. setTimeout is there, but setImmediate is not. You’ll use the Deno global namespace for a lot of OS-level interactions.

Need to read a file? You’ll use the web-standard FileReader API or Deno’s own APIs.

// Using Deno's specific API (synchronous for example brevity)
const data = Deno.readTextFileSync("./data.json");
console.log(data);

// Using a more web-standard way (async)
const file = await Deno.open("./data.json");
// ... work with the file stream

Another common pitfall is expecting CommonJS modules to work. They don’t. Deno is ESM-only, and frankly, the JavaScript ecosystem is finally moving that way. This is a good thing, but it can be a migration headache if you’re bringing over old code.

The Configuration Escape Hatch: deno.json

While Deno works with zero config, sometimes you want config. You might need a special compiler option, or you want to define tasks. For that, there’s deno.json (or deno.jsonc). This file can act as your tsconfig.json, your package.json’s scripts, and your linter config all in one.

{
  "tasks": {
    "start": "deno run --allow-net server.ts",
    "dev": "deno run --watch --allow-net server.ts"
  },
  "compilerOptions": {
    "strict": true
  }
}

Now you can just run deno task start. It’s a clean, centralized way to manage your project without the clutter of a dozen dotfiles.

So, is Deno the future? For many projects, especially new ones, it absolutely is. It takes the best parts of the modern JavaScript ecosystem—ES modules, top-level await, security-by-default—and bundles them into a ruthlessly efficient tool that respects your time. You just have to be willing to let go of the node_modules safety blanket. Trust me, you won’t miss it.