Right, so you’ve decided to give Deno a spin. Good for you. You’ve escaped the gravitational pull of node_modules and its 200,000-file orbit. But Deno isn’t just Node.js without the baggage; its two most defining features, its permission model and its standard library, are a completely different philosophy. One is a paranoid security guard, and the other is a well-stocked toolbox you don’t have to beg npm for. Let’s get you acquainted with both.

The Permission Model: Your Code on a Leash

This is the part that feels most alien coming from Node. In Node, if your code can run, it can do anything: read your files, phone home to Uzbekistan, send all your emails. It’s a free-for-all. Deno says, “Absolutely not.” By default, Deno scripts have no permissions. None. Try this script:

// read_local_file.ts
const fileContent = await Deno.readTextFile("./secrets.txt");
console.log(fileContent);

Now run it:

deno run read_local_file.ts

You’ll be greeted with a glorious, screaming error: Requires read access to "./secrets.txt". Deno just prevented your code from doing something you didn’t explicitly allow. This is a feature, not a bug. It forces you to consciously grant permissions at runtime, which is a fantastic way to avoid “oops, I didn’t know that dependency was logging my entire filesystem” situations.

You grant permissions using flags. Need to read from a specific directory? Be specific.

# Allow reading from anywhere (probably too broad)
deno run --allow-read read_local_file.ts

# Better: allow reading only from the current directory
deno run --allow-read=. read_local_file.ts

The list of permissions is extensive: --allow-net for network access, --allow-env for environment variables, --allow-run for subprocesses, and so on. You can even scope them down to specific values, like --allow-net=api.github.com to only allow calls to GitHub’s API. This granularity is what makes it powerful. It’s not just security theater; it’s a genuine constraint that makes you think about your code’s capabilities. The best practice? Always use the most specific permission possible. It’s a hassle for about a week, and then it becomes second nature.

The Standard Library: No Left-Pad Needed

Remember the last time you needed a UUID, a hash function, or to format a date? You went to npm, found five packages, chose the one that looked least abandoned, and added it to your project. Deno’s maintainers find this absurd. So, they provide a robust, audited, and versioned standard library (std) for common tasks that shouldn’t be external dependencies.

You don’t install it. You just import it directly from a URL. Yes, a URL. It looks weird at first, but you get used to it.

// import_uuid.ts
import { generate as generateUuid } from "https://deno.land/std@0.177.0/uuid/mod.ts";

const myUuid = await generateUuid();
console.log(`Here's your UUID: ${myUuid}`);

Run it with deno run --allow-net import_uuid.ts. Wait, --allow-net? For a UUID library? Yep. The first time you run this, Deno will download and cache the standard library module from that URL. That’s why it needs network access. Subsequent runs will use the cache and won’t need the permission. This is the Deno way: remote imports are cached indefinitely, leading to reproducible builds.

The std library is vast, covering everything from file system parsing (std/fs), to HTTP (std/http), to datetime manipulation (std/datetime), and even useful data structures like a binary heap (std/collections). The quality is generally high, but here’s the insider’s truth: it’s not all perfectly polished. Some modules are fantastic; others feel a bit like an afterthought. Always check the documentation. The huge advantage is that it’s all written in TypeScript, so the types are first-class citizens, not an afterthought.

See that @0.177.0 in the import URL? That’s not just a version; it’s your lifeline. The standard library is still considered unstable (versioned 0.x.x), meaning the maintainers reserve the right to break things. Pinning to a specific version in your import is non-negotiable. If you just import from "https://deno.land/std/uuid/mod.ts", you’re asking for your build to break unexpectedly when they release a new version.

The other common pitfall? Forgetting that some standard library functions are async and others are sync. The documentation is your best friend here. There’s no magic; you just have to read it. For example, the std/path module is entirely synchronous because it’s just string manipulation, while std/fs is almost entirely async because it’s actually hitting the disk.

Here’s a more complex example using the HTTP server and a file read, showcasing permissions and the standard library working together:

// server.ts
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";

const server = serve({ port: 8000 });
console.log("HTTP server running on http://localhost:8000");

for await (const req of server) {
  // Try to read a file related to the request URL
  const filePath = `.${req.url}.txt`;
  try {
    const content = await Deno.readTextFile(filePath);
    req.respond({ status: 200, body: content });
  } catch {
    req.respond({ status: 404, body: "File not found, pal." });
  }
}

To run this, you need to be very specific with your permissions. It needs network access to serve and filesystem access to read.

deno run --allow-net=localhost:8000 --allow-read=. server.ts

This grants network access only on port 8000 and read access only to the current directory. If a request tries to read ../../etc/passwd, the permission model we set up will block it. That’s the power of the combination: the standard library gives you the tools, and the permission model ensures they can’t be misused.