Skip to main content

The basics of Deno

Basic deno commands

  • deno upgrade: upgrades deno to latest stable version
  • deno init <project-name>: scaffolds out a typescript project
  • deno uninstall <package>: uninstalls a package
  • deno install: installs all dependencies
  • deno add <package>: installs a package
  • deno remove <package>: removes a package

Running files and permissions

  • deno run <file>: runs a file
  • deno run --watch <file>: runs file in watch mode

You also have these security options in place, because by default deno prevents reading and writing from files. To override this behavior, use these flags:

  • deno run -A <file>: bypasses all security
  • deno run -R <file>: allows reading
  • deno run -W <file>: allows writing
  • deno run -E <file>: allows access environment variables
  • deno run -N <file>: allows access to the network.

To deny reading and writing to specific files or directories, you can use these options:

  • --deny-read=<filepath>: denies reading the specified filepath or folderpath
  • --deny-write=<filepath>: denies writing to the specified filepath or folderpath

Deno scripts

To run a specific script from your deno.json, you would use the deno run command and then provide the script name:

deno.json
{
"tasks": {
"dev": "deno run -A --watch main.ts"
},
}
deno task <task-name>

In the above example, you would run deno task dev.

Running tasks asynchronously

You can run two tasks asynchronously or concurrently by connecting them with an ampersand.

# runs task1 and task2 concurrently
deno task task1 & deno task task2

Environment variables

To load environment variables from a .env file into your project so you can access them during runtime, you need to use the --env option when running a file with deno:

deno run --env main.ts

You can get environment variables programmatically with Deno.env.get(var_name).

Format, linting, type checking

  • deno fmt: formats your code
  • deno lint: lints your code
  • deno lint --fix: fixes incompatibility with node modules
  • deno check: type checks your code

For linting, you can lint specific files and folders like so:

deno lint <file-or-folder-path>

You can also tell the deno linter to ignore specific files by adding the // deno-lint-ignore-file comment at the top of your file:

main.ts
// deno-lint-ignore-file

// ... rest of your code

Watch mode

You can supply the --watch flag to deno rundeno test, and deno fmt to enable the built-in file watcher. The watcher enables automatic reloading of your application whenever changes are detected in the source files.

deno run --watch main.ts
deno test --watch
deno fmt --watch

Here are all the options associated with watch mode:

  • --watch: enables watch mode
  • --watch-exclude=<files>: accepts a comma-separated list of filepaths or glob paths to avoid watching.

When in watch mode, you can also exclude certain files from being watched using the --watch-exclude option:

deno run --watch --watch-exclude=file1.ts,file2.ts main.ts
deno run --watch --watch-exclude='*.js' main.ts

Compile into executable

deno compile

  • deno compile <file>: compiles a js file into an executable
    • -A: compile with all permissions
    • -o <filename>: rename executable to this filename

deno bundle

The deno bundle command is used to bundle frontend static assets into a single JS file for distribution or works like deno compile if not used for frontend:

  • Resolves and inlines all dependencies
  • Supports JSX/TSX, TypeScript, and modern JavaScript, including import attributes and CSS
  • HTML entrypoint support (Deno 2.5+)
  • Optional minification (--minify) and source maps (--sourcemap)
  • Code splitting
  • Platform targeting (--platform, supports Deno and browser)
  • JSX support when configured

CLI

The below example bundles main.ts into an output bundle.js

deno bundle -o bundle.js main.ts

Here are the options:

FlagDescription
-o--output <file>Write bundled output to a file
--outdir <dir>Write bundled output to a directory
--minifyMinify the output for production
--format <format>Output format (esm by default)
--code-splittingEnable code splitting
--platform <platform>Bundle for browser or deno (default: deno)
--sourcemapInclude source maps (linkedinlineexternal)
--watchAutomatically rebuild on file changes
--inline-importsInline imported modules (true or false)

HTML entrypoint support 

Starting with Deno 2.5, deno bundle supports HTML files as entrypoints.

deno bundle --outdir dist index.html

When you use an HTML file as an entrypoint, deno bundle will:

  1. Find all script references in the HTML file
  2. Bundle those scripts and their dependencies
  3. Update the paths in the HTML file to point to the bundled scripts
  4. Bundle and inject any imported CSS files into the HTML output

Given an index.tsx file:

index.tsx
import { render } from "npm:preact";
import "./styles.css";

const app = (
<div>
<p>Hello World!</p>
</div>
);

render(app, document.body);

And an HTML file that references it:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
<script src="./index.tsx" type="module"></script>
</head>
</html>

Running deno bundle --outdir dist index.html produces:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
<script src="./index-2TFDJWLF.js" type="module" crossorigin></script>
<link rel="stylesheet" crossorigin href="./index-EWSJYQGA.css">
</head>
</html>

The bundled output includes content-based hashes for cache-busting and fingerprinting.

NOTE

HTML entrypoints are fully supported in both the CLI and the runtime API.

runtime bundling

The Deno.bundle() method lets you bundle files during runtime:

const result = await Deno.bundle({
entrypoints: ["./index.tsx"],
outputDir: "dist",
platform: "browser",
minify: true,
});
console.log(result);

Deno Config

The deno.json has a bunch of important things that determine the behavior of the deno environment and type checking:

Compiler options

The compiler options change the typescript environment using and other things. It is basically the same thing as the tsconfig.json options:

TypeScript environment

  • "compilerOptions"."lib": this key specifies the typescript library types to use, and accepts an array of strings that represent the environment types to add:
    • "dom": adds frontend and DOM types
    • "deno.ns": adds types based on the Deno namespace, allowing you to have correct types for when using the Deno class and any of its methods.
    • "deno.worker": adds types for web workers for the Worker() MDN spec
    • "esnext": gives you the types for the latest javascript features.
{
"compilerOptions": {
"lib": ["dom", "deno.ns"] // for SSR apps (frontend + backend)
}
}

To specify the library files to use in a TypeScript file, you can use /// <reference lib="..." /> comments:

/// <reference no-default-lib="true" />
/// <reference lib="dom" />

Standard typescript settings

{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true
}
}

Linting settings

The "lint" key has settings that affect the behavior of deno lint:

deno.json
{
"lint": {
"include": ["src/"],
"exclude": ["src/testdata/", "src/fixtures/**/*.ts"],
"rules": {
"tags": ["recommended"],
"include": ["ban-untagged-todo"],
"exclude": ["no-unused-vars"]
}
}
}
  • "lint"."include": lists the files to include for linting
  • "lint"."exclude": lists the files to exclude for linting

Formatting settings

deno.json
{
"fmt": {
"useTabs": true,
"lineWidth": 80,
"indentWidth": 4,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve",
"include": ["src/"],
"exclude": ["src/testdata/", "src/fixtures/**/*.ts"]
}
}
  • "fmt"."include": lists the files to include for formatting
  • "fmt"."exclude": lists the files to exclude for formatting

Excluding paths from type checking, linting, formatting

To exclude files and folders from being type checked, linted, or formatted by Deno at all, use the top level "excludes" key:

{
"exclude": [
// exclude the dist folder from all sub-commands and the LSP
"dist/"
]
}

Importing modules

From NPM

When trying to install a package from npm, you have to specify that it's from npm by prefixing the package name with npm:.

deno add npm:chalk

You can then use it like so:

import chalk from "chalk";
console.log(chalk.red("bruh"));

Or you could just import like so, removing the need to explicitly install a package:

import chalk from "npm:chalk";

When downloading type declarations

When you download a package that needs to use type declarations, you can just specify the type declarations package like so instead of separately downloading it.

// @deno-types="npm:@types/express@^4.17"
import express from "npm:express@^4.17";

// @ts-types="npm:@types/lodash"
import * as _ from "npm:lodash";

Importing native node modules

When importing native node modules, you need to prefix them with node:

// ❌
import * as fs from "fs";
import * as http from "http";

// ✅
import * as fs from "node:fs";
import * as http from "node:http";

Importing online modules

When importing packages from an online URL, you can directly import modules inline from the URL or use the "imports" key in the deno.json to refer to that url via a different name:

Deno also supports import statements that reference HTTP/HTTPS URLs, either directly:

import { Application } from "https://deno.land/x/oak/mod.ts";

or part of your deno.json import map:

{
"imports": {
"oak": "https://deno.land/x/oak/mod.ts"
}
}

NOTE

HTTP imports are not supported by deno add/deno install commands.

Import remappings

When using deno add <package-name> you can use the "imports" key in the deno.json as a key-value map between library identifier shorthands and their package resolution url:

{
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.0",
"chalk": "npm:chalk@5"
}
}

Then your script can use the bare specifier std/assert:

import { assertEquals } from "@std/assert";
import chalk from "chalk";

assertEquals(1, 2);
console.log(chalk.yellow("Hello world"));

custom remappings for files

You can have custom remappings just like in tsconfig.json:

The import map in deno.json can be used for more general path mapping of specifiers. You can map an exact specifiers to a third party module or a file directly, or you can map a part of an import specifier to a directory.

deno.json
{
"imports": {
// Map to an exact file
"foo": "./some/long/path/foo.ts",
// Map to a directory, usage: "bar/file.ts"
"bar/": "./some/folder/bar/"
}
}

Usage:

import * as foo from "foo";
import * as bar from "bar/file.ts";

Custom remapping of folders

Just like in typescript, you can remap entire folderpaths to a different folderpath:

For example:

deno.json
{
"imports": {
"@/": "./"
}
}

And then you can refer to files under that folder like so:

import { MyUtil } from "@/util.ts";

This causes import specifiers starting with @/ to be resolved relative to the import map's URL or file path.

Importing files

Importing typescript files

When importing TypeScript files, you must add on the .ts extension.

// WRONG: missing file extension
import { add } from "./calc";

// CORRECT: includes file extension
import { add } from "./calc.ts";

Importing asset files

Json files

You can import JSON files as javascript objects like so:

import data from "./data.json" with { type: "json" };

console.log(data.property); // Access JSON data as an object

Text files

All text files (csv, txt, log, etc.) are imported as strings:

import text from "./log.txt" with { type: "text" };

console.log(typeof text === "string");
// true
console.log(text);
// Hello from a text file

media

You can statically import any media asset as a UInt8Array instance:

import bytes from "./image.png" with { type: "bytes" };

console.log(bytes instanceof Uint8Array);
// true
console.log(bytes);
Uint8Array(12) [
// 72, 101, 108, 108, 111,
// 44, 32, 68, 101, 110,
// 111, 33
// ]

Jupyter notebooks in deno

You can add deno kernels to jupyter notebook with the deno jupyter --install command. Once you're there, yoiu can start running cells with Deno

Importing files

The important thing to know about importing TS files into deno is that you can only import them via absolute path (NOT FILE URL).

Development workflow

Deno skills

title: "GitHub - denoland/skills: Modern Deno skills for AI coding assistants. Covers Deno, JSR imports, Fresh, Deno Deploy, and best practices."
image: "https://avatars.githubusercontent.com/u/3490640?s=64&v=4"
description: "Modern Deno skills for AI coding assistants. Covers Deno, JSR imports, Fresh, Deno Deploy, and best practices. - denoland/skills"
url: "https://github.com/denoland/skills"
favicon: ""
aspectRatio: "100"

There are a list of skills the official Deno team supplies so that agents always have an up to date way of working with Deno:

SkillDescription
deno-guidanceCore Deno best practices, JSR packages, CLI commands
deno-deployDeployment workflows for Deno Deploy
deno-frontendFresh framework, Preact components, Tailwind CSS
deno-sandboxSafe code execution with @deno/sandbox
deno-project-templatesProject scaffolding templates
deno-expertCode review and debugging principles
Using the npx skills library, you can install the skill like so:
npx skills add https://github.com/denoland/skills --skill deno-expert

Deno with docker

Dockerfile

Here is a standard image setup for deno with docker

FROM denoland/deno:latest

WORKDIR /app

# Copy manifests first so the dependency install layer caches across
# source-only edits
COPY deno.json deno.lock package.json* ./
RUN deno ci --prod --skip-types

# Then copy the rest of the source
COPY . .

CMD ["deno", "run", "--allow-net", "main.ts"]

To tighten security and run as non-root user, use the following commands:

# Create deno user
RUN addgroup --system deno && \
adduser --system --ingroup deno deno

# Switch to deno user
USER deno

# Continue with rest of Dockerfile

Development with docker compose

services:
app:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgres://deno:${POSTGRES_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
command:
[
"deno",
"run",
"--allow-net=db:5432",
"--allow-env=DATABASE_URL",
"main.ts",
]

db:
image: postgres:16-alpine
environment:
POSTGRES_USER: deno
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U deno"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped

volumes:
pgdata: