jsguides

Getting started with Deno: a modern JavaScript and TypeScript runtime

Overview

Getting started with Deno is straightforward. Deno is a modern JavaScript and TypeScript runtime built by the original creator of Node.js. It runs JavaScript and TypeScript directly, has no node_modules directory by default, and locks in security through explicit permissions. Where Node.js requires separate tools for formatting, linting, and testing, Deno ships them all built in.

Deno 2 added Node.js and npm compatibility, so existing npm packages work inside Deno. The toolchain stays focused: you get deno run, deno test, deno lint, deno fmt, and deno compile out of the box.

Installation

On macOS and Linux:

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

Looking at these two examples side by side helps you decide which pattern to use in your own code. The first shows the minimal version that gets the job done, while the second adds error handling or edge-case coverage that you will want in any non-trivial application. Choose based on how much safety your code needs.

The shell command below processes the output from the previous block. Running both steps together forms a complete workflow, where each command builds on the result of the one before it.

On Windows (PowerShell):

irm https://deno.land/install.ps1 | iex

The output shown in the next block confirms that the code works as intended, but it is worth comparing the result structure across both examples. Small differences in the return value or log format often reveal important assumptions about how the underlying API behaves. Noticing those details now saves debugging time later.

The shell command below processes the output from the previous block. Running both steps together forms a complete workflow, where each command builds on the result of the one before it.

Or via package managers:

# Homebrew
brew install deno

# Chocolatey
choco install deno

Both blocks demonstrate related techniques, but they differ in an important detail. The first example shows the basic pattern, while the one that follows adds a layer of complexity that you will encounter in real projects. Pay attention to how the setup differs between the two, because that difference determines when to use each approach.

The shell command below processes the output from the previous block. Running both steps together forms a complete workflow, where each command builds on the result of the one before it.

Verify the installation:

deno --version
# deno 2.x.x

The example shown above covers one specific scenario, but the code you will write in production often needs to handle additional edge cases. The block that follows extends the pattern to a related situation, showing how the same approach adapts when the inputs or expected outputs change.

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Running your first script

Create a file called hello.ts:

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

Looking at these two examples side by side helps you decide which pattern to use in your own code. The first shows the minimal version that gets the job done, while the second adds error handling or edge-case coverage that you will want in any non-trivial application. Choose based on how much safety your code needs.

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Run it:

deno run hello.ts
# Hello from Deno!

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

No build step. No compiler flags. Deno handles TypeScript natively.

Permissions

Deno blocks access to the network, file system, and environment by default. A script that tries to read a file without permission fails:

// read_file.ts — this will fail
const text = await Deno.readTextFile("/etc/passwd");
console.log(text);

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Running the command processes the code from the previous block and produces visible output. The result confirms that the setup was correct and the logic executed as intended.

deno run read_file.ts
# Error: Permission denied (os error 63)
#   Hint: Use --allow-read flag to allow file system access

The output shown in the next block confirms that the code works as intended, but it is worth comparing the result structure across both examples. Small differences in the return value or log format often reveal important assumptions about how the underlying API behaves. Noticing those details now saves debugging time later.

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Grant specific permissions with flags:

deno run --allow-read=/tmp hello.ts
# Only /tmp is readable, other paths are blocked

Common permission flags:

FlagWhat it allows
--allow-readFile system read
--allow-writeFile system write
--allow-netNetwork access
--allow-envEnvironment variables
--allow-runRunning sub-processes
--allow-allEverything (avoid in production)

You can also scope permissions to specific hosts:

deno run --allow-net=api.example.com fetch_data.ts

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

The deno.json Configuration File

The deno.json (or deno.jsonc) file configures your project. Create one in your project root:

{
  "compilerOptions": {
    "allowJs": true,
    "lib": ["deno.window"],
    "strict": true
  },
  "tasks": {
    "dev": "deno run --watch --allow-net main.ts",
    "start": "deno run --allow-net --allow-env main.ts"
  },
  "imports": {
    "@oak/oak": "jsr:@oak/oak@^14"
  },
  "exclude": ["**/*.test.ts"]
}

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

The tasks section defines shortcuts. Instead of typing the full command:

deno task dev
# runs: deno run --watch --allow-net main.ts

The npm command shown here installs dependencies or executes scripts defined in your project configuration. The output confirms whether the operation completed successfully.

The imports section registers JSR or npm specifiers for your project.

Dependencies and JSR

Deno uses JSR, a JavaScript registry with native TypeScript support. Import packages directly from their JSR or npm specifiers:

// Using JSR package
import { oak } from "jsr:@oak/oak@^14";

// Using npm package
import express from "npm:express@4";

The npm command shown here installs dependencies or executes scripts defined in your project configuration. The output confirms whether the operation completed successfully.

No package.json, no node_modules. Deno caches downloads and locks them in deno.lock.

Update the lock on dependency changes:

deno cache --lock=deno.lock --lock-write main.ts

Both blocks demonstrate related techniques, but they differ in an important detail. The first example shows the basic pattern, while the one that follows adds a layer of complexity that you will encounter in real projects. Pay attention to how the setup differs between the two, because that difference determines when to use each approach.

The npm command shown here installs dependencies or executes scripts defined in your project configuration. The output confirms whether the operation completed successfully.

Using npm Packages

Deno 2 supports npm packages directly:

import express from "npm:express@4";
import { z } from "npm:zod@3";

const app = express();

app.get("/", (req, res) => {
  res.send("Hello from Deno with npm packages!");
});

app.listen(3000);

Deno installs them into a local node_modules directory when package.json is present. You can also import npm packages directly without a package.json using the npm: specifier.

Running a web server

Deno ships with web platform APIs , fetch, Request, Response, Headers , available globally, no imports needed.

// server.ts
const handler = async (req: Request): Promise<Response> => {
  const url = new URL(req.url);

  if (url.pathname === "/api/hello") {
    const name = url.searchParams.get("name") ?? "World";
    return Response.json({ message: `Hello, ${name}!` });
  }

  return new Response("Not found", { status: 404 });
};

Deno.serve({ port: 8000 }, handler);

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Running the command processes the code from the previous block and produces visible output. The result confirms that the setup was correct and the logic executed as intended.

deno run --allow-net server.ts
# Server running at http://localhost:8000

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Deno.serve() is the native HTTP server. There’s no need to import an external package.

File system access

Read and write files with the Deno global:

// Write to a file
const encoder = new TextEncoder();
const data = encoder.encode("Hello, Deno!\n");
await Deno.writeFile("hello.txt", data);

// Read back
const text = await Deno.readTextFile("hello.txt");
console.log(text);  // Hello, Deno!

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Running the command processes the code from the previous block and produces visible output. The result confirms that the setup was correct and the logic executed as intended.

deno run --allow-write --allow-read write_read.ts

The example shown above covers one specific scenario, but the code you will write in production often needs to handle additional edge cases. The block that follows extends the pattern to a related situation, showing how the same approach adapts when the inputs or expected outputs change.

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Environment Variables

Read environment variables with Deno.env:

const port = Deno.env.get("PORT") ?? "8000";
const dbUrl = Deno.env.get("DATABASE_URL");

console.log(`Server starting on port ${port}`);

// Only allow specific env vars
// deno run --allow-env=PORT,DATABASE_URL server.ts

Looking at these two examples side by side helps you decide which pattern to use in your own code. The first shows the minimal version that gets the job done, while the second adds error handling or edge-case coverage that you will want in any non-trivial application. Choose based on how much safety your code needs.

The following code builds on the concepts introduced above, adding another layer of functionality. Seeing the progression from simple to more complex helps clarify when to introduce each technique.

Built-in Testing

Write and run tests with Deno.test:

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

// math.test.ts
import { assertEquals } from "jsr:@std/assert";
import { add } from "./math.ts";

Deno.test("adds two numbers", () => {
  assertEquals(add(2, 3), 5);
});

Deno.test("handles negatives", () => {
  assertEquals(add(-1, 1), 0);
});

The output shown in the next block confirms that the code works as intended, but it is worth comparing the result structure across both examples. Small differences in the return value or log format often reveal important assumptions about how the underlying API behaves. Noticing those details now saves debugging time later.

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Run tests:

deno test
# Check file:///path/to/math.test.ts
# ok ... adds two numbers
# ok ... handles negatives

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

The @std/assert package provides test assertions. Deno has a built-in test runner with built-in assertion functions , no external test framework required for simple tests.

Lint and Format

Deno includes a linter and formatter:

# Format all TypeScript files
deno fmt

# Check for lint errors
deno lint

Both blocks demonstrate related techniques, but they differ in an important detail. The first example shows the basic pattern, while the one that follows adds a layer of complexity that you will encounter in real projects. Pay attention to how the setup differs between the two, because that difference determines when to use each approach.

The JSON configuration shown here wires together the pieces defined above. Each setting maps to a specific behavior, so the file serves as both documentation and runtime configuration.

Configure linting and formatting rules in deno.json:

{
  "lint": {
    "include": ["src/"],
    "rules": {
      "tags": ["recommended"]
    }
  },
  "fmt": {
    "useTabs": true,
    "lineWidth": 80,
    "singleQuote": true
  }
}

The example shown above covers one specific scenario, but the code you will write in production often needs to handle additional edge cases. The block that follows extends the pattern to a related situation, showing how the same approach adapts when the inputs or expected outputs change.

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

Hot reload during development

Use --watch to restart on file changes:

deno run --watch --allow-net server.ts

Running the command executes the code with the specified permissions and flags. Watch the output to confirm the operation succeeded and to understand any side effects the command produced.

The server restarts automatically whenever you save a change.

Compiling to an Executable

Deno can compile your script into a standalone binary:

deno compile --allow-net --allow-env -o my_server server.ts

This produces a native executable with Deno embedded. No runtime needed on the target machine.

Deno vs Node.js

FeatureDenoNode.js
TypeScriptNative, no configRequires ts-node or compilation
package.jsonOptionalRequired
node_modulesAuto-generated, optionalRequired
Built-in toolsLint, format, test, compileNone , external packages needed
PermissionsSecure by defaultFull access by default
ESM-firstYesCommonJS default
npm supportYes (Deno 2+)Yes

Working in a real project

The nicest part of Deno is that a project can stay small without feeling unfinished. A single deno.json file can describe formatting, linting, tasks, and import paths, so the setup is easier to scan than a stack of separate tool configs. That makes the project more approachable when you come back to it after a break.

Deno also encourages a habit of naming tasks explicitly. Instead of remembering a long command by hand, you can define a task for running the server, another for tests, and another for release checks. That small bit of structure helps a team stay consistent and gives new contributors a safer place to start. The project feels less like a bundle of commands and more like a guided workflow.

Permissions as a design choice

Deno’s permission model works best when you decide access up front. If a script reads files, talks to the network, or needs environment variables, make those needs visible in the command you run. That is not just a security feature. It also documents the expectations of the program in a way that is hard to miss during review.

When a script fails because a permission is missing, the error message usually points you toward the fix quickly. Treat that as part of the development loop. It forces you to think about what the program truly needs, which often leads to cleaner boundaries and fewer accidental dependencies.

When Deno fits best

Deno shines when you want a modern TypeScript-first runtime with a small amount of ceremony. It is a good match for utilities, APIs, and internal tools that benefit from built-in formatting and testing. It is also easy to teach because the runtime itself includes so much of the standard workflow.

That said, it is still worth checking whether your team already depends on Node-specific tooling or packages. If the surrounding stack is heavily tied to Node, the migration cost can outweigh the benefits. The best fit is the one that lowers the total friction for the project, not the one that looks newest on paper.

Day-to-Day Workflow

The day-to-day Deno experience is smoothest when the project tasks are explicit. One command should start the app, one should run the tests, and one should check formatting or linting. That keeps the development loop predictable and means you do not need to remember a long list of flags every time you return to the code.

It also helps to keep scripts short and descriptive. A reader should be able to tell what a task does just from the name. When the workflow is easy to explain, contributors are more likely to use it consistently, and the project stays easier to maintain as it grows.

Small project habits

The most helpful Deno projects usually have a few simple habits that never change. One of those habits is making the entry points obvious. If a script starts a server, runs a test suite, or checks a build step, the command name should make that clear. That gives people a better first impression and lowers the chance that they will run the wrong task by mistake.

Another useful habit is keeping the import layout tidy. Deno already reduces tool noise, so the rest of the code should be easy to scan. Short files with direct imports and a small number of shared helpers tend to age well. They are easier to revisit, easier to debug, and easier to hand off to someone new.

See Also