Everything I know about Deno
45 min read
12197 words
Hey and welcome everyone to this guide on Deno! I'm so excited to have you here. In this guide, we're going to dive deep into Deno, exploring its features, APIs, and tools. By the end of this guide, you'll have a solid understanding of Deno and be ready to start building your own projects with it.
This guide is a living document, which means I'll be updating it regularly with new content and examples. I'm here to help you learn and answer any questions you might have. So, let's get started!
If you're interested in being notified about new parts or in general about things I do and write about, head over to my newsletter and subscribe. I would really appreaciate it!
Topics that will be added to this guide:
- Introduction to Deno
- Setting up Deno
- Deno toolchain
- Deno on the CLI (added October 7th, 2024)
- Deno and the file system (added November 18th, 2024)
- Deno and HTTP
- Deploying Denos
What is Deno
Letβs start with the official definition of the Deno website and break it up afterwards:
Deno is the open-source JavaScript runtime for the modern web. Built on web standards with zero-config TypeScript, unmatched security, and a complete built-in toolchain, Deno is the easiest, most productive way to JavaScript.
At its heart, Deno is a runtime that lets us run JavaScript, much like Node.js if you're familiar with that. But here's where it gets interesting: Deno is built with TypeScript in mind. We'll be using TypeScript a lot in this guide, and I'll show you why that's actually a good thing, even if you're more comfortable with plain JavaScript.
Now, I know what you might be thinking: "Great, another JavaScript runtime to learn." But trust me, Deno has some tricks up its sleeve that make it really stand out. We're going to talk about its approach to security, which is pretty cool. It uses these things called runtime flags that give you more control over what your code can do.
And here's something I'm personally excited about - Deno comes with a bunch of built-in tools that make our lives as developers so much easier. No more spending hours setting up your development environment!
Throughout this guide, we'll explore all of these aspects of Deno. I'm here to guide you, answer your questions, and hopefully make the learning process enjoyable. So, ready to start this Deno journey with me?
How is this Guide structured
I've designed this Deno guide to be your fast track into the world of Deno, focusing on the APIs and tools you'll use most often in your projects.
My goal? To get you up and running with Deno as quickly as possible. I've distilled the most important topics into a concise package, giving you everything you need to get a solid grasp of Deno. Think of it as an 80/20 approach (Pareto Principle) - we're covering the 80% of Deno that you'll use most of the time. If you want to know more about this type of approach, check out the Pareto principle article on Wikipedia.
Before we dive in, let's talk prerequisites. You'll need:
- A good foundation in JavaScript
- Ideally, some experience with TypeScript
Don't worry if you're not quite there yet! If you need to brush up on your JavaScript, we highly recommend the JavaScript course at freeCodeCamp.org. For TypeScript, the official TypeScript documentation is an excellent starting point.
Once you've got those basics down, you're all set to jump into Deno with us!
I've built this guide drawing from multiple sources:
- The official Deno documentation
- Various supporting websites
- Open-source projects
- My personal experiences with Deno
Throughout the guide, I'll provide plenty of in-depth resources for each topic. These are there to help you dive deeper into areas that particularly interest you or that you find challenging.
Ready to get your feet wet with Deno? Let's dive in!
Install deno and set it up
Let's get Deno up and running on your machine! The installation process is straightforward, and I'll walk you through it step by step for different platforms.
macOS / Linux
For macOS and Linux users, it's as simple as running a shell script. Here's how you do it:
curl -fsSL https://deno.land/install.sh | sh
This script automatically detects your system and installs the appropriate Deno binary.
π¨ Safety First: Before running any script from the internet, it's crucial to review its contents. Even if it's from a trusted source like Deno's official website, it's a good habit to verify what you're executing on your system.
Homebrew
It is also possible to install Deno via homebrew. It is as simple as the script above:
brew install deno
π‘ Note: You have to note that upgrading Deno via homebrew is not done with the
deno upgrade
command. Instead you have to use brew upgrade deno
.
Windows
Windows users, you're not left out! The process is just as straightforward:
irm https://deno.land/install.ps1 | iex
This PowerShell command downloads and runs the Deno installation script tailored for Windows.
Docker
For the Docker enthusiasts out there, Deno offers a variety of official images. These are based on popular distributions, giving you flexibility in your containerized environments.
Here's a quick reference of available Docker images:
Base | Docker Tag |
---|---|
Alpine Linux | denoland/deno:alpine |
Debian | denoland/deno:debian (default) |
Distroless | denoland/deno:distroless |
Ubuntu | denoland/deno:ubuntu |
Binary only | denoland/deno:bin |
For more details on Dockerfiles and usage, check out the Deno Docker GitHub repository.
Need More Info?
If you want to dive deeper into installation options or troubleshoot any issues, the official Deno installation guide is an excellent resource. There youβll find alternative installations as well.
Now that we've covered installation, are you ready to start building with Deno? Let's move on to your first Deno project!
Setting Up Your Deno Development Environment
One of Deno's great features is its built-in language server (LSP), making it compatible with a wide range of editors. While we'll focus on three popular options here, remember that there are many more. For a comprehensive list, check out the official Deno docs on Setting Up Your Environment.
Visual Studio Code: The Popular Choice
VSCode users, you're in luck! There's an official Deno extension waiting for you in the Visual Studio Marketplace.
Here's how to set it up:
- Install the extension from the marketplace.
- Open the command palette (Cmd/Ctrl + Shift + P).
- Type and select "Deno: Initialize Workspace Configuration".
- Answer a few quick questions, and you're done!
This creates a configuration file in the .vscode
subdirectory, perfect for
switching between Deno, Bun and Node.js projects, as the settings are only valid
for this workspace.
JetBrains IDEs: For the IntelliJ Lovers
If you're a fan of JetBrains IDEs, you're covered. The official Deno extension is available in the JetBrains Marketplace.
Setup is straightforward:
- Install the extension in your IDE.
- Settings should auto-configure, but you might need to tweak a few things.
For a detailed guide, check out the WebStorm Blog post on Deno support.
Zed: The New Kid on the Block
Zed users, don't feel left out! While the setup might feel a bit trickier, it's definitely doable.
Here's your game plan:
- Open the command palette and search for "zed: extensions".
- Find and install the community-built Deno extension.
- Create a
.zed/settings.json
file in your workspace with the following content:
{
"languages": {
"TypeScript": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint"
]
},
"TSX": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint"
]
}
}
}
This configuration tells Zed to use Deno's LSP instead of the standard TypeScript ones. Need more help? I've got a detailed guide on setting up the Deno language server in Zed that I keep updated.
Remember, whichever editor you choose, the goal is the same: a smooth, efficient Deno development experience. Happy coding and now letβs set up our project and start right in.
The Deno Toolchain
The deno init
Command
Similar to Node.js projects where we get a project going with e.g. npm init
,
Deno provides an init
command to set up the essential files in your directory.
Here's how you can use it:
deno init my-deno-project
When run without arguments (deno init
), it creates three default files in your
current directory. If you provide an additional argument (like my-deno-project
in the example above), Deno will create a folder with that name and place these
files inside.
The Three Default Files
Let's see what Deno creates for us:
-
main.ts
- This is the entry point of your app, similar toindex.js
orapp.js
in Node.js projects. It's where your application's execution begins. However, you're not restricted to this name and can choose freely. -
main_test.ts
- As the_test
part suggests, this is where your tests live. Deno has built-in testing capabilities, which we'll cover in more detail later. This file contains a basic test for the functionality inmain.ts
. It's good to give the test files the same name as the file that is tested. -
deno.json
- This is the configuration file for a Deno project, similar topackage.json
in Node.js, but with key differences. Here, you can configure various aspects of your project:- Import maps
- Scripts
- Formatter settings
- Linter rules
- TypeScript compiler options
The
deno.json
file is central to customizing your Deno development environment. - Import maps
Running a Deno Project
Once we've initialized our project, we can run it using the deno run
command:
deno run main.ts
Give the files created by default this command will execute our main.ts
file
with Add 2 + 3 = 5
printed to the console.
One key difference from Node.js is that Deno doesn't require a package manager
or a node_modules
folder. All dependencies are fetched and cached on first
run, making project setup much simpler. However if we would like to use a npm
package this possible. How it's done is described in the next chapter about
using external modules in Deno.
π Further Documentation:
Working with External Modules in Deno
Deno has a unique and refreshing approach to including modules and external code. This approach simplifies dependency management and makes your projects more portable.
URL-based Imports
One of the standout features of Deno is its ability to import modules directly from URLs. You can pull code straight from GitHub, or any other source that serves JavaScript or TypeScript files, without needing to install it first. Here's an example:
import { format } from "https://raw.githubusercontent.com/denoland/std/main/fmt/duration.ts";
console.log(format(99674, { style: "digital" }));
In this example, we're importing the format
function directly from Deno's
standard library hosted in raw format on github.com. When running this script
the output should be 00:00:01:39:674:000:000
. There we can see, that we could
work without a package management system after all.
Caching Mechanism
Similar to tools like yarn
or pnpm
Deno uses a local cache of all the
dependencies it has to download. Deno will download and cache modules the first
time you run your script or application. Therefore we need to download them once
and are able to use them in every project without needing a redownload.
When Deno encounters a URL import, it follows these steps:
- Check if the module is in the local cache.
- If not, download the module and store it in the cache.
- Use the cached version for subsequent runs.
This caching mechanism ensures that your code runs consistently and doesn't unnecessarily re-download modules. The cache is stored in a directory that respects your operating system's conventions:
- On Linux/macOS:
$HOME/.cache/deno/
- On Windows:
%LOCALAPPDATA%\deno\
(usuallyC:\Users\{UserName}\AppData\Local\deno\
)
If you can't find a directory there, try using deno cache
which should give
you a similar output like the following:
β― deno info
DENO_DIR location: /Users/user/Library/Caches/deno
Remote modules cache: /Users/user/Library/Caches/deno/deps
npm modules cache: /Users/user/Library/Caches/deno/npm
Emitted modules cache: /Users/user/Library/Caches/deno/gen
Language server registries cache: /Users/user/Library/Caches/deno/registries
Origin storage: /Users/user/Library/Caches/deno/location_data
There we can see that my remote modules cache can be found under
/Users/user/Library/Caches/deno/deps
. If we take a look on the contents of the
directory, we see how Deno manages those remote modules:
β ls /Users/user/Library/Caches/deno/deps/https/
cdn.jsdelivr.net examples.deno.land jsr.io
cdn.sheetjs.com fresh.deno.dev raw.githubusercontent.com
deno.land github.com unpkg.com
esm.sh
Inside of those directories we can find a lot of different files that represent
our JavaScript or Typescript files. If we take a look into one of them we can
see that it is a 1:1 copy. For example looking into
/Users/user/Library/Caches/deno/deps/https/raw.githubusercontent.com/
we might
find our duration.ts
file used earlier. However the file name is in this case
a hash so that Deno can be sure that it knows the file or has to download it.
As you might have read that we have a "npm modules cache" there. We will learn other potential sources of modules in a later chapter. You can be sure, that those will be stored in the cache in the same way.
Security Considerations
While URL imports are powerful, they do raise security concerns. What if the content at a URL changes or becomes malicious? Deno addresses this with several features:
-
Lockfile: You can generate a lockfile (
deno.lock
) that records the hashes of your dependencies. Deno will verify these hashes on subsequent runs. You can usedeno cache --lock=deno.lock <file>
to do this manually. -
Integrity Checking: You can specify a hash in your import statement to ensure you're getting the exact file you expect:
import { serve } from "https://raw.githubusercontent.com/denoland/std/main/fmt/duration.ts#sha256=abcdef...";
-
Permissions Model: Deno has a robust permissions model that we'll cover in more detail later. It prevents scripts from accessing resources (like the network or file system) without explicit permission.
Using npm Modules
Deno isn't limited to URL-based modules. It can use npm
modules directly,
bridging the gap between the Deno and Node.js ecosystems. To use an npm module,
you use the npm:
specifier in front of the module name. For example:
import express from "npm:express";
const app = express();
app.get("/", function (req, res) {
res.send("Hello World");
});
app.listen(3000);
Running the script in Deno will result in setting up a simple HTTP server with express. This feature allows you to leverage the vast npm ecosystem while enjoying the benefits of Deno's modern runtime.
If you're trying this example, you'll see similar prompts to this:
β― deno run main.ts -A
β β οΈ Deno requests read access to <CWD>.
β β Requested by `Deno.cwd()` API.
β β Learn more at: https://docs.deno.com/go/--allow-read
β β Run again with --allow-read to bypass this prompt.
β Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >
This is the implementation of Deno's security model. We have to explicitly give Deno our permission to use different APIs. The message gives you a lot of hints on where to look for more information. We'll cover the permission model in detail in our Deno on the CLI chapter.
Managing Dependencies
Unlike Node.js with its package.json
, Deno doesn't require a central file
listing all dependencies. Instead, dependencies are specified directly in your
code through imports. This approach has several advantages:
- Clear Dependency Tree: You can easily see which file is using which dependency.
- No "Hidden" Dependencies: All dependencies are explicitly imported where they're used.
- Easy to Remove Unused Dependencies: If you remove an import, you've removed the dependency.
However, if you want to manage versions of your dependencies centrally, you can
use import maps. These allow you to specify versions for your imports in a JSON
file, similar to package.json
. Here's an example:
{
"imports": {
"colors": "https://raw.githubusercontent.com/denoland/std/main/fmt/colors.ts"
}
}
With this import map, you can simply use import { bold } from "colors";
in
your code, and Deno will use the version specified in the import map.
By default Deno uses a deno.json
file with the imports
attribute as a
default way to manage imported modules.
Tradeoffs and what the HTTP imports might bring into your projects Did Ryan Dahl summarize in a blog post.
π Further Documentation
JavaScript's (Not) Missing Standard Library
One of the long-standing criticisms of JavaScript has been its lack of a comprehensive standard library that doesn't rely on external dependencies. Deno addresses this issue head-on by providing a well-crafted standard library maintained by its core developers and community contributors.
The Deno Standard Library
Deno's standard library, referred to as std
, is a collection of high-quality,
well-tested modules that cover a wide range of functionalities. These modules
are designed to work seamlessly with Deno and follow its design principles. A
lot of the modules work in NodeJS, Bun or even in the browser.
Key aspects of the Deno standard library:
-
No External Dependencies: All modules in the standard library are implemented without relying on external packages.
-
TypeScript First: The entire library is written in TypeScript, providing excellent type checking and autocompletion in supported editors.
-
Browser APIs: Instead of using custom implementations the library focuses on implementing features with standard Web APIs to ensure compatibility with other runtimes like Bun or Node.
-
Modular Design: Each module is small and focused, allowing you to import only what you need.
-
Continuous Improvement: The library is actively maintained and improved by the Deno core team and community contributors.
Example Modules in the Standard Library
Let's explore a couple of modules in Deno's standard library:
-
fs: File system operations
import { readJson, writeJson } from "https://deno.land/std/fs/mod.ts"; const data = await readJson("config.json"); await writeJson("output.json", data);
-
path: Path manipulation utilities
import { join } from "https://deno.land/std/path/mod.ts"; const fullPath = join("directory", "subdirectory", "file.txt");
-
http: HTTP server and client utilities
import { serve } from "https://deno.land/std/http/server.ts"; serve((req) => new Response("Hello, World!"));
-
datetime: Date and time utilities
import { format } from "https://deno.land/std/datetime/mod.ts"; console.log(format(new Date(), "yyyy-MM-dd"));
-
testing: Assertion library for Deno's built-in test runner
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; Deno.test("example test", () => { assertEquals(2 + 2, 4); });
Benefits of Using the Standard Library
Using Deno's standard library offers several advantages:
- Consistency: The modules are designed to work together seamlessly.
- Performance: They're optimized for use with Deno. (However they work in other runtimes)
- Built-in TypeScript Support: No need for separate
@types
packages.
Versioning and Stability
It's worth noting that the standard library is versioned independently of Deno itself. When importing from the standard library, you specify a version:
import { serve } from "https://deno.land/std@0.140.0/http/server.ts";
This allows you to lock to a specific version of the standard library, ensuring consistency across different environments and over time.
Since importing standard library modules via the "deno.land" url is deprecated, we will now talk about how we import them correctly.
π Further Documentation
JSR - JavaScript Registry
JSR, or JavaScript Registry, is an exciting new project initiated by the Deno team. It aims to provide a modern, TypeScript-first package registry that addresses the limitations of npm while being usable across different JavaScript runtimes.
Key Features of JSR
-
TypeScript-First: JSR is designed with TypeScript in mind, making it easier to publish and consume TypeScript packages.
-
Cross-Runtime Compatibility: Packages on JSR can be used not only in Deno, but in Node.js, browsers, and other JavaScript environments.
-
Integrated Documentation: JSR automatically generates documentation from the published modules, making it easier for users to understand and use the packages.
-
Transparency: Package contents are visible directly on the JSR website, increasing trust and making it easier to audit dependencies.
-
Quality Scoring: JSR provides a scoring system that indicates the quality of a package based on factors like documentation, compatibility, and typing.
-
Dependency Information: Clear visibility of both dependencies and reverse dependencies (packages that depend on a given package).
Using JSR in Deno
Using a package from JSR in your Deno project is straightforward:
import { format } from "jsr:@std/fmt/duration";
console.log(format(99674, { style: "digital" }));
Using the first example we did here, we can substitute the long github.com link
with jsr:@std/fmt/duration
- Deno does the rest for us. Downloading, caching
and providing the duration package for us.
JSR vs npm
While JSR isn't meant to replace npm, it offers several advantages:
- TypeScript-Native: No need for separate
@types
packages or compilation steps. - Simpler Publishing Process: No need for a separate build step before publishing.
- Better Transparency: Easy to see what's in a package before using it.
- Cross-Runtime by Design: Packages are designed to work across different JavaScript environments.
The Future of JSR
As of now, JSR is in its early stages, but it shows great promise. The Deno team plans to make it a community-driven project, with governance that involves the broader JavaScript community. This approach aims to ensure that JSR evolves to meet the needs of the entire JavaScript ecosystem, not only Deno users.
fmt
Deno's fmt
command is your go-to tool for code formatting. It's inspired by
tools like Go's go fmt
command, aiming to provide a standardized and
opinionated way to format your code.
Under the hood
Deno uses a Rust-based formatter called
dprint
. This ensures fast and consistent
formatting across your project.
What can it format?
Deno's formatter supports all the file types you'll commonly use in a Deno project:
- JavaScript (
.js
) and TypeScript (.ts
) - JSX (
.jsx
) and TSX (.tsx
) - Markdown (
.md
,.markdown
) - JSON (
.json
) and JSON with comments (.jsonc
)
π‘ Since Deno 1.46 we have support for HTML, CSS, SCSS, Sass, Less, YAML, Astro,
Angular, Svelte and Vue files as well. The formatter for those file types is in
an unstable state for now. Update: Since Deno 2.0.0-rc.10 the formatter for
those file types is stable. You can find the PR #25753
here. This Deno version also adds
support for .vto
and .njk
files. Which are often used in the lume static generator.
Ignoring formatting
Sometimes you might want to keep a specific piece of code formatted in a certain way. Deno allows for this:
-
To ignore formatting for a specific line:
// deno-fmt-ignore const messyButIntentionalCode = doSomethingComplicated( );
-
To ignore an entire file:
// deno-fmt-ignore-file
Customizing the formatter
You can tailor the formatter to your project's needs by adding a fmt
section
to your deno.json
or deno.jsonc
file:
{
"fmt": {
"useTabs": true,
"lineWidth": 80,
"indentWidth": 4,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve",
"include": ["src/"],
"exclude": ["src/testdata/", "data/fixtures/**/*.ts"]
}
}
This configuration allows you to:
- Use tabs instead of spaces
- Set a maximum line width
- Define indentation width
- Choose whether to use semicolons
- Prefer single quotes over double quotes
- Control how markdown prose is wrapped
- Specify which directories to include or exclude from formatting
By setting up your formatting rules, you ensure that your entire team is working with a consistent code style, which can improve readability and reduce merge conflicts.
π Further Documentation
lint
Deno comes with its own linter, accessible via the deno lint
command. This
tool goes beyond formatting your code - it helps ensure consistent coding
practices across your project. Think of it as your personal code quality
assistant. It behaves similar to popular tools like eslint
What does the Deno linter do?
The Deno linter checks for a variety of potential issues in your code. Examples are:
- Consistent use of quotation marks (single vs. double)
- Proper usage of
const
instead ofvar
orlet
when variables aren't reassigned - And many more code quality checks
In total, Deno provides 104 linting rules as of this writing. Out of these, 76 are recommended and enabled by default. This means you get a solid foundation for code quality right out of the box.
Configuring the linter
Like many Deno tools, you can configure the linter in your deno.json
or
deno.jsonc
file. This allows you to set project-wide linting preferences.
Taken from the official documentation is the following example to include or exclude rules or files:\
{
"lint": {
"include": ["src/"],
"exclude": ["src/testdata/", "src/fixtures/**/*.ts"],
"rules": {
"tags": ["recommended"],
"include": ["ban-untagged-todo"],
"exclude": ["no-unused-vars"]
}
}
}
For a comprehensive list of all available lint rules, check out the deno_lint documentation.
Ignoring linter rules
Sometimes, you might need to bypass certain linter rules. Deno makes this easy:
-
To ignore linting for an entire file:
// deno-lint-ignore-file
-
To ignore multiple rules for a file:
// deno-lint-ignore-file no-explicit-any no-empty
-
To ignore a rule for a specific line:
// deno-lint-ignore no-explicit-any const myVar: any = getSomeValue();
Here's a neat feature: you can add comments explaining why you're ignoring a rule:
// deno-lint-ignore no-explicit-any -- we really need to use 'any' here due to [reason]
const complexData: any = fetchDataFromExternalAPI();
This is great for maintaining code clarity, when working in teams.
π Further Documentation
doc
Documentation is crucial for any project, and Deno makes it easy to generate comprehensive documentation for your code.
How it works
Deno's documentation generator works by reading the JSDoc
comments in your code. JSDoc is a markup language used to annotate JavaScript
source code files. When you run deno doc
, it parses these comments and
generates documentation based on them.
Basic usage
To generate documentation for a file, run:
deno doc your_file.ts
This will output the documentation to the console. However, there are more useful ways to use this feature.
Generating HTML documentation
You can create a static HTML website of your documentation:
deno doc --html --name="My Project" main.ts
This command:
- Uses the
--html
flag to generate HTML output - Sets the name of your project with
--name
- Specifies the entry point of your project (
main.ts
)
By default, this creates a docs
folder in your current directory with the
generated HTML files.
JSON output
If you prefer machine-readable output, you can generate JSON:
deno doc --json main.ts
This is useful if you want to process the documentation data programmatically.
Linting your documentation
Deno can help ensure your documentation is complete. Use the --lint
flag:
deno doc --lint main.ts
This will warn you about missing documentation or inconsistencies.
For example:
error[missing-jsdoc]: exported symbol is missing JSDoc documentation
--> /Users/user/code/main.ts:8:14
|
8 | export const myFunc = (str: string) => {
| ^
error: Found 1 documentation lint error.
Read more about what the --lint
finds in the
official documentation
Best practices for documentation
- Document all exported symbols (functions, classes, interfaces, etc.).
- Provide clear, concise descriptions for each item.
- Include examples where appropriate.
- Specify types for parameters and return values.
Here's an example of well-documented code:
/**
* Represents a user in the system.
*/
export interface User {
/**
* The user's full name.
*/
name: string;
/**
* Unique identifier for the user.
*/
id: number;
}
/**
* Retrieves the username from a User object.
* @param {User} user - The user object.
* @returns {string} The user's name.
* @example
* const user = { name: "John Doe", id: 1 };
* const name = getUsername(user);
* console.log(name); // Outputs: "John Doe"
*/
export function getUsername(user: User): string {
return user.name;
}
By following these practices and using Deno's documentation tools, you can ensure your project is well-documented and easy for others to understand and use.
π Further Documentation
test & coverage
Testing is a crucial part of any software development process, and Deno makes it incredibly easy to get started. With Deno's built-in testing suite, you have no excuse not to test your code!
Writing your first test
Let's start with a simple example. Say we have a function that adds two numbers
(like the function that we get after running deno init
):
// main.ts
export function add(a: number, b: number): number {
return a + b;
}
We can write a test for this function like so:
// main_test.ts
import { assertEquals } from "jsr:@std/assert";
import { add } from "./main.ts";
Deno.test(function addTest() {
assertEquals(add(2, 3), 5);
});
Here's what's happening:
- We import the
assertEquals
function from Deno's standard library. - We import our
add
function from the main file. - We use
Deno.test
to define a test case. - Inside the test, we use
assertEquals
to check if our function produces the expected result.
Running tests
To run your tests, use the deno test
command in your terminal. Deno will
automatically find and run all files with names ending in _test.ts
or
_test.js
.
Asynchronous tests
Deno makes it easy to test asynchronous code too. Put an async
keyword in
front of your function parentheses in your test:
Deno.test("async function test", async () => {
const result = await someAsyncFunction();
assertEquals(result, expectedValue);
});
This way we can even test functions that work with Promises such as API calls for example.
More advanced testing
Deno's testing framework allows for more complex scenarios. You can use
t.step()
to create subtests within a larger test:
Deno.test("math operations", async (t) => {
await t.step("addition", () => {
assertEquals(add(2, 3), 5);
});
await t.step("multiplication", () => {
assertEquals(multiply(2, 3), 6);
});
});
This structure helps organize related tests and provides more granular test results. For example creating a user in a database before working with this test user.
Test Documentation Written in JSDoc
Deno also supports running tests and type checks inside JSDoc strings. The only thing we need is to put the code to run inside a block of code in the comment. Below is a sample doc string that shows how it's done.
/**
* reverse is a function to reverse a string.
*
* # Examples
* ```ts
* import { assertEquals } from "jsr:@std/assert/equals";
*
* const reversed = reverse("hello");
* assertEquals(reversed, "olleh");
* ```
*/
export function reverse(s: string): string {
return s.split("").reverse().join("");
}
If we now run deno test --doc
(the --doc
is important here), we can see in
the output that Deno also executes the examples in our doc strings.
β deno test --doc
Check file:///Users/user/deno/main.ts$42-48.ts
Check file:///Users/user/deno/main.ts$57-63.ts
running 1 test from ./main.ts$42-48.ts
file:///Users/user/deno/main.ts$42-48.ts ... ok (0ms)
running 1 test from ./main.ts$57-63.ts
file:///Users/user/deno/main.ts$57-63.ts ... ok (0ms)
ok | 2 passed | 0 failed (12ms)
This is a cool feature when writing libraries, for example, to show people examples and also have tests written at the same time.
If you want to ignore the example from testing you can use the ignore
attribute next to the declaration of the code block language.
/**
* reverse is a function to reverse a string.
*
* # Examples
* ```ts ignore
* import { assertEquals } from "jsr:@std/assert/equals";
*
* const reversed = reverse("hello");
* assertEquals(reversed, "olleh");
* ```
*/
export function reverse(s: string): string {
return s.split("").reverse().join("");
}
This feature was implemented in PR #25220 - so itβs pretty fresh right in time for Deno 2.0.
Test coverage
Deno provides built-in test coverage analysis. To use it, run your tests with
the --coverage
flag:
deno test --coverage
This will create a coverage profile. You can then use the deno coverage
command to analyze this profile:
deno coverage
This will show you which parts of your code are covered by tests and which
aren't. You can even generate a detailed HTML report of your coverage with the
help of lcov and it's genhtml
tool:
deno coverage --lcov --output=cov_profile
genhtml -o cov_profile/html cov_profile/lcov.info
This creates an interactive HTML page showing your code coverage, making it easy to identify areas that need more testing.
π Further Documentation
bench
Performance is crucial in many applications, and Deno provides a built-in benchmarking tool to help you measure and optimize your code's performance.
What is benchmarking?
Benchmarking is the process of measuring the performance of a piece of code. It helps you understand how fast your code runs and can be crucial for identifying bottlenecks or comparing different implementations.
Basic usage
Let's start with a simple benchmark. Say we have a function we want to measure:
// main.ts
export const add = (a: number, b: number): number {
return a + b;
}
We can create a benchmark for this function like this:
// bench.ts
import { add } from "./main.ts";
Deno.bench("addition benchmark", () => {
add(2, 3);
});
To run this benchmark, use the deno bench
command:
deno bench bench.ts
Deno will run your benchmark multiple times and provide statistics about its performance.
Understanding the output
When you run a benchmark, you'll see output in a table similar to this:
cpu: Apple M1
runtime: deno 1.40.3 (aarch64-apple-darwin)
file:///Users/user/project/bench.ts
benchmark time (avg) iter/s (min β¦ max) p75 p99 p995
-------------------------------------------------- -----------------------------
addition benchmark 4.08 ns/iter 245,136,869.3 (3.99 ns β¦ 9.9 ns) 4.09 ns 4.35 ns 4.4 ns
This output tells you:
- The average time per iteration
- How many iterations can be performed per second
- The range of times (minimum to maximum)
- The 75th, 99th, and 99.5th percentiles
When you have multiple files with multiple benchmarks Deno will go through all of them and run them one by one. This might take a moment but will result in output like this:
β deno bench -A
Check file:///Users/user/code/projects/deno-course/code/bench/read_bench.ts
Check file:///Users/user/code/projects/deno-course/code/bench/sort_bench.ts
cpu: Apple M1
runtime: deno 1.46.1 (aarch64-apple-darwin)
file:///Users/user/code/projects/deno-course/code/bench/read_bench.ts
benchmark time (avg) iter/s (min β¦ max) p75 p99 p995
------------------------------------------------------------------------------------ -----------------------------
readPokedex 3.73 ms/iter 268.0 (3.61 ms β¦ 4.19 ms) 3.76 ms 4.19 ms 4.19 ms
readPokedex with start and end 6.82 Β΅s/iter 146,692.1 (6.25 Β΅s β¦ 26.46 Β΅s) 6.75 Β΅s 11 Β΅s 26.46 Β΅s
file:///Users/user/code/projects/deno-course/code/bench/sort_bench.ts
benchmark time (avg) iter/s (min β¦ max) p75 p99 p995
----------------------------------------------------------------------- -----------------------------
Array.sort Arr1 92.29 ns/iter 10,835,411.7 (88.67 ns β¦ 105.1 ns) 94.04 ns 102.99 ns 103.9 ns
Array.sort Arr2 100.98 ns/iter 9,902,601.0 (97.12 ns β¦ 124.71 ns) 102.72 ns 107.85 ns 110.35 ns
MergeSorting Arr1 854.86 ns/iter 1,169,784.5 (839.41 ns β¦ 921.54 ns) 858.71 ns 921.54 ns 921.54 ns
MergeSorting Arr2 1.07 Β΅s/iter 938,260.4 (1.05 Β΅s β¦ 1.14 Β΅s) 1.07 Β΅s 1.14 Β΅s 1.14 Β΅s
quickSorting Arr1 569.39 ns/iter 1,756,266.6 (557.1 ns β¦ 631.91 ns) 574.1 ns 631.91 ns 631.91 ns
quickSorting Arr2 724.42 ns/iter 1,380,410.7 (710.2 ns β¦ 764.66 ns) 729.67 ns 764.66 ns 764.66 ns
More complex benchmarks
You can create more sophisticated benchmarks using additional options:
Deno.bench({
name: "complex addition benchmark",
fn: () => {
add(Math.random() * 1000, Math.random() * 1000);
},
baseline: true,
group: "math operations",
});
This benchmark:
- Has a custom name
- Uses a more complex function (with random numbers)
- Is marked as a baseline (useful for comparing different implementations)
- Belongs to a group (helpful for organizing related benchmarks even across files)
Benchmarking asynchronous code
Similar or even same as the asynchronous test
functions we can even benchmark
asynchronous functions. This is done in the same way putting the async
in
front of the parentheses. Regular Javascript syntax.
Deno.bench("async operation", async () => {
await someAsyncOperation();
});
Controlling benchmark execution
Sometimes, you might want to control what part of your code is being timed. You
can do this using b.start()
and b.end()
:
Deno.bench("partial operation", (b) => {
// Setup code (not timed)
const data = prepareData();
b.start();
// This is the part that will be timed
processData(data);
b.end();
// Cleanup code (not timed)
cleanupData(data);
});
This allows you to exclude setup and cleanup time from your benchmark.
π‘ Note: Calling the benchmarking parameter b
is your choice.
If you want to know more about the deno bench
command. There is a lot of
documentation about this in the
deno bench article
and in the Deno.bench API docs.
Best practices for benchmarking
- Run benchmarks multiple times to account for variability.
- Be aware of environmental factors (CPU load, etc.) that might affect results.
- Compare benchmarks on the same machine for consistency.
- Use realistic data and operations in your benchmarks.
By using Deno's benchmarking tools, you can gain valuable insights into your code's performance and make informed optimizations.
π Further Documentation
compile
One of my favorite built in tools of Deno is a powerful tool that allows you to create standalone executables from your TypeScript or JavaScript code. This can be incredibly useful for distributing your applications to users who don't have Deno installed.
Basic usage
To compile a Deno application, use the deno compile
command:
deno compile main.ts
This will create an executable file named after your source directory (e.g.,
foo
or foo.exe
on Windows). The binary is default for your CPU architecture
of your host machine by default.
Cross-compilation
One of the most powerful features of Deno's compiler is its ability to cross-compile for different platforms. You can create executables for Windows, macOS, or Linux, regardless of your development platform.
To specify a target, use the --target
flag:
deno compile --target x86_64-pc-windows-msvc main.ts
This would create a Windows executable, even if you're developing on macOS or Linux.
Available targets include:
x86_64-unknown-linux-gnu
for x86 (Intel or AMD) Linuxaarch64-unknown-linux-gnu
for ARM-based Linuxx86_64-pc-windows-msvc
for x86 Windowsx86_64-apple-darwin
for Intel-based macOSaarch64-apple-darwin
for ARM-based Macs
π‘ Note: You can find the architecture that Deno is running on when checking the
Deno --version
command.
Specifying permissions
When compiling, you need to specify all the permissions your application will need at runtime. For example:
deno compile --allow-read --allow-write main.ts
This creates an executable with permissions to read from and write to the file system.
Considerations
While Deno's compile feature is powerful, there are a few things to keep in mind:
-
File size: Compiled executables can be large (50MB or more) because they include the Deno runtime.
-
Performance: There might be a slight startup delay compared to running with
deno run
, as the executable needs to unpack the bundled code. -
Updates: The compiled executable is static. If you update your dependencies, you'll need to recompile.
-
Platform specificity: While you can cross-compile, the resulting executable is specific to the target platform and architecture.
Best practices
-
Minimize dependencies: The more external dependencies your project has, the larger the compiled executable will be.
-
Test thoroughly: Make sure to test your compiled executable in an environment similar to where it will be deployed.
-
Version your executables: Keep track of which source code version each executable was compiled from.
-
Document required permissions: Make sure users know what system access your application requires.
Example: Creating a CLI tool
Let's say you've created a useful command-line tool with Deno. Here's how you might compile and distribute it:
// cli.ts
import { parse } from "https://deno.land/std/flags/mod.ts";
const { args } = Deno;
const parsedArgs = parse(args);
if (parsedArgs.help) {
console.log("Usage: cli [options]");
Deno.exit(0);
}
console.log("Hello from my CLI tool!");
// ... rest of your CLI logic
To compile this:
deno compile cli.ts
π Further Documentation
Understanding import.meta
import.meta
is a powerful feature in modern JavaScript that provides metadata
about the current module. It's part of the ECMAScript specification and is
supported in modern browsers and JavaScript runtimes, including Deno.
What is import.meta?
import.meta
is an object that contains contextual information about the module
in which it's accessed. The exact properties available on import.meta
can vary
depending on the environment, but Deno provides several useful properties.
Key Properties in Deno
-
import.meta.url
: This provides the URL of the current module. In Deno:- For local scripts (e.g.,
deno run main.ts
), it's afile://
URL pointing to the script's location on the filesystem. - For remote scripts, it's the full URL of the script.
- For local scripts (e.g.,
-
import.meta.main
: This boolean property istrue
if the current module is the main module that was directly run by Deno. It's used to write code that should execute when a file is run directly, not when it's imported by another module. -
import.meta.resolve(specifier)
: This method resolves a module specifier relative to the current module.
Practical Uses of import.meta
Let's look at practical examples of how you might use import.meta
in your Deno
projects:
-
Determining if a module is the main module:
if (import.meta.main) { console.log("This code only runs if this module is the main module"); }
This is even done in the
main.ts
file after runningdeno init
. -
Getting the directory of the current module:
import { dirname } from "jsr:@std/path"; const currentDir = dirname(new URL(import.meta.url).pathname); console.log(`This module is located in: ${currentDir}`);
import.meta vs __dirname and __filename
If you're coming from Node.js, you might be used to __dirname
and
__filename
. Deno doesn't have these globals, but you can achieve the same
results with import.meta.url
:
import { fromFileUrl } from "jsr:@std/path";
const __filename = fromFileUrl(import.meta.url);
const __dirname = new URL(".", import.meta.url).pathname;
This approach is more explicit about the source of these values and works consistently across different environments.
π Further Documentation
Wrapping Up the Deno Toolchain
Concluding this whole chapter of the Deno toolbox you can see that we're getting a lot out of the box. The whole project is set up in a way that developers can start writing code right away without the need of setting up Typescript, linting, formatting or anything. It all just works.
In the next chapter we will talk about using Deno as a runtime to build a simple CLI tool. We'll learn about the permission model that makes applications more secure through disabling any permissions (e.g. network, filesystem) by default having to explicitly giving the script our allowance.
Deno on the CLI
Get ready to dive into the powerful world of Deno's security model and command line features! In this chapter, we'll explore how Deno's permissions system keeps your applications secure, learn how to use environment variables, and discover the art of parsing command line arguments.
We'll also uncover techniques for user interaction, gain insight into the host system, and even add a splash of color to your console output. In the end, you'll have the tools to create secure, interactive, and visually appealing Deno applications that stand out from the crowd. Let's unlock the full potential of the Deno runtime!
The Permission Model
A major drawback of NodeJS is that it will run any code that is launched by
default with full permissions. This means that malicious code can run on our
host without Node doing anything about it. Fortunately, this is not the case
with Deno unless you explicitly give it the necessary permissions. For example,
the --allow-net
flag only allows network activities such as fetch
requests.
There are two flags responsible for allowing access to the file system.
--allow-read
and --allow-write
. Without these two flags, the application can
try to read/write, but Deno will not allow it. This gives us an extra layer of
security for our applications and host environments.
All available flags can be found in the documentation.
Specify flags
To avoid an "all or nothing" scenario and give our applications either full access or none at all, it is possible to specify the flags.
For example, we can customize the --allow-net
flag to give access to
example.com
only. This can be done by passing domains to the
--allow-net=example.com
flag. If we try to make a request to mydomain.com
,
Deno will ask us if we allow it.
Let's take the following example:
// main.ts
const resExample = await fetch("https://example.com");
const resDeno = await fetch("https://deno.com");
console.log(resExample);
console.log(resDeno);
If we run the short script with deno run --allow-net=example.com main.ts
, we
will see Deno asking us for more permissions. This is because we only allowed
access to example.com
and not deno.com
.
β deno run --allow-net=example.com main.ts
β β οΈ Deno requests net access to "deno.com:443".
β β Requested by `fetch()` API.
β β Learn more at: https://docs.deno.com/go/--allow-net
β β Run again with --allow-net to bypass this prompt.
β Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >
We can restrict access to URLs or IPs, including ports. If we know in advance
which domains will be used, we can provide our applications with a specific
--allow-net
flag for increased security.
See the documentation for more examples and information on restrictions for other flags.
Request access in apps
To prevent our applications from crashing unnecessarily while running, it is
possible to check whether permissions are set or not. In the following example,
we can check if it is possible for the application to access example.com
.
const desc1 = { name: "net", host: "example.com" } as const;
console.log(await Deno.permissions.query(desc1));
await Deno.permissions.request(desc1);
console.log(await Deno.permissions.query(desc1));
As we will see below, the status is initially prompt
, which means that the
question has not yet been asked, so we need to use Deno.permissions.request
to
ask the user if it is okay to access example.com
. If it is confirmed with Y
,
we will see in the next Deno.permissions.query
call that the
state: "granted"
is set. Therefore we can now assume that we can communicate
with example.com
via network requests.
PermissionStatus { state: "prompt", onchange: null }
β
Granted net access to "example.com".
PermissionStatus { state: "granted", onchange: null }
The same is also available for read
and write
, so that we can, for example,
determine where we are allowed to write data or have to take a different path.
More information can be found in the
Deno docs on the Permissions API
or the
Permissions Management Example on Deno by Examples.
Using Environment Variables
Sooner or later, our applications will need to access the host's environment
variables. We can access them if the --allow-env
flag was passed when the
application was started. The operation is then done by simply calling the
Deno.env
API. For example, Deno.env.get("MY_VARIABLE")
.
A cool feature is Deno.env.toObject()
- this gives us a complete object of all
environment variables, where the key is the variable name and the value is the
corresponding value of that variable. It would be possible to get the above
variable like this
const { MY_VARIABLE } = Deno.env.toObject();
In many scenarios (e.g. in development) a lot of work is done with .env
files.
There is a module for this in the std
library under @std/dotenv
.
This works with an .env
file as follows:
# .env
MY_VARIABLE=MY_VALUE
In the application, the .env
files can then be accessed as follows.
import { load } from "jsr:@std/dotenv";
// 1st solution
const env = await load();
console.log(env["MY_VARIABLE"]);
// 2nd solution
await load({ export: true });
console.log(Deno.env.get("MY_VARIABLE"));
// 3rd solution
await load({ export: true });
const { MY_VARIABLE } = Deno.env.toObject();
console.log(MY_VARIABLE);
All three options have the same effect. The last two will export the variables
to the underlying host at runtime. We need to make sure that Deno is also called
with the --allow-read
flag so that the .env
file can be read.
Another nice security feature that Deno provides is the ability to restrict the
variables that the application can access. For example, an application can only
receive the PORT
and HOST
environment variables. In this case, all other
host environment variables will be ignored or not passed to the application. To
prevent the application from reading variables, it can be called with
--allow-env=PORT,HOST
. All other environment variables will then be
inaccessible.
Parsing Command Line Arguments
No CLI tool is complete without variable input parameters to control the application. Accordingly, Deno offers the possibility to use and define arguments out of the box.
For this we have the parseArgs
method from the std/cli
library. This method
uses the Deno.args
API to read the passed parameters and define them in a
meaningful object.
We have the option to define the possible arguments via a config. You can choose
between Boolean, Collections or Strings. It offers the same features as
minimist
, which was used as an
inspiration. In addition, we can define defaults, aliases and negatable
variables.
The following is an extended example from Deno by Example, which includes a few more arguments.
import { parseArgs } from "jsr:@std/cli/parse-args";
const flags = parseArgs(Deno.args, {
boolean: ["help", "color"],
string: ["version"],
default: { color: true },
negatable: ["color"],
collect: ["books"],
alias: { help: "h" },
});
In the example we see that there are two boolean variables. One is help
which
shows the help output (--help
) and the other is --color
which can be used,
for example, if you want to get colored output.
The advantage of this is that you do not have to enter --help true
or
--help=true
, this is seen as the default if no value is given.
The --version
flag can be filled with a variable string. For example,
--version=1.0.0
.
With the --books
flag it is possible for us to record entries directly from
the user, which can then be processed as an array. For example:
--books alchemist --books 1984 --books "Atomic Habits"
gives us an array of 3
strings [ "alchemist", 1984, "Atomic Habits" ]
which we can process directly.
Negatable variables mean, for example, that instead of --color=false
it is
possible to write --no-color
. The functionality remains the same. A --no-
is
added before the flag.
An alias is quickly explained. This gives us the option of calling up a flag
under different names. In this case, we have the option of using --help
or
--h
.
As an example we can look at the extended Deno by Example code:
import { parseArgs } from "jsr:@std/cli/parse-args";
const flags = parseArgs(Deno.args, {
boolean: ["help", "color"],
string: ["version"],
default: { color: true },
negatable: ["color"],
collect: ["books"],
alias: { help: "h" },
});
console.log("Wants help?", flags.help);
console.log("Version:", flags.version);
console.log("Wants color?:", flags.color);
console.log("Books:", flags.books);
console.log("Other:", flags._);
When we put this in a file called flags.ts
for example, and run it with some
random values for our flags, we get the output of:
β deno run flags.ts --help --books alchemist --books 1984 --books="Atomic Habits" --version=2.0.0 MyValue
Wants help? true
Version: 2.0.0
Wants color?: true
Books: [ "alchemist", 1984, "Atomic Habits" ]
Other: [ "MyValue" ]
What we can see here is that we have a value in our Other
section. This comes
from flags._
where everything that is not recognized as a flag is placed.
Prompt the user
From time to time user input is required within the application. Here we can use features that are available in almost all browsers (because Deno only uses browser technology, right?).
We can use prompt
, alert
and confirm
to get input from the user.
Worth mentioning are alert
and confirm
, two nice helper functions that allow
us to confirm (by pressing the Enter key) for alert
, or to confirm the input
of a y
or N
(confirm
) without any effort. The latter returns a boolean
value that we can use in our applications to find out whether the user has made
a decision for or against the question.
// main.ts
alert("Be informed!");
const c = confirm("Are you sure?");
console.log("is the user sure?", c);
β deno run -A flags.ts
Be informed! [Enter]
Are you sure? [y/N] y
is the user sure? true
Host Interaction
We have in the Deno namespace some APIs that allow us to get information about the host system running our application.
For example, we can get the host name (Deno.hostname()
), the uptime of the
host (Deno.osUptime()
), general information about the memory of the host or
the running Deno process (Deno. systemMemoryInfo()
or Deno.memoryUsage()
),
the process identifier (Deno.pid
and Deno.ppid
) or the path used by the Deno
executable (Deno.execPath()
).
An example of commands and their example outputs:
console.log("Hostname:", Deno.hostname());
console.log("uptime:", Deno.osUptime());
console.log("system Memory", Deno.systemMemoryInfo());
console.log("build:", Deno.build);
console.log("version:", Deno.version);
// Hostname: MacBook.local
// uptime: 52661
// system Memory [Object: null prototype] {
// total: 17179869184,
// free: 2177296,
// available: 7781456,
// buffers: 0,
// cached: 0,
// swapTotal: 0,
// swapFree: 0
// }
// build: {
// target: "aarch64-apple-darwin",
// arch: "aarch64",
// os: "darwin",
// vendor: "apple",
// env: undefined
// }
// version: { deno: "2.0.0-rc.10", v8: "12.9.202.13-rusty", typescript: "5.6.2" }
We also get information about the current working directory we are in
(Deno.cwd()
). This is because Deno allows us to interact with the file system
and create, read or modify files. Additionally we can change file permissions or
owners with the help of a Deno application as well.
A more detailed explanation of how Deno interacts with a host's file system can be found in a later section "Deno and the filesystem".
Colorful Message Logging
A good feature that is helpful for CLI applications is the ability to use simple
CSS for console.log
output. This can replace a library like
chalk
in some places.
For example, we can change the font color, decoration or size. This feature is available in many browsers, including Deno.
For this we need %c
symbols that signal to the runtime where changes should
take place, e.g. we can use console.log("Hello %cWorld%c", "color: green")
to
print the word "World" in green and thus indicate the success of a command in a
CLI application, for example.
In addition to the simple color names, it is possible to specify these in hex or
RGB values. Various changes can be defined by a string separated by a semicolon.
E.g.
console.log("Hello %cWorld", "color: green; text-decoration: underline; font-weight: bold")
.
π Further Documentation
Wrapping up Deno on the CLI
In this chapter, we've explored the robust security features and powerful CLI capabilities that set Deno apart. We've covered:
- Deno's permissions model, which provides granular control over application access
- Secure use of environment variables
- Parsing command line arguments for flexible user input
- Interacting with users through prompts and alerts
- Access host system information
- Add visual flair with colorful console output
These features combine to make Deno a secure, versatile, and easy-to-use runtime for building modern CLI applications.
Deno and the file system
Many applications interact with the host file system in some way. For this we have several functions in Deno that are provided to us.
In order to use these in our applications, we need to remember to use the
runtime flags, such as --allow-read
and/or --allow-write
.
In the following chapter we will look at how to interact with the filesystem,
work with directories or manage files. At the end of the chapter we will create
an application, similar to ls
, which can give us information about
directories.
Interact with Directories
Checking the contents of a folder
If we want our application to know what is in a folder, Deno offers us an easy
way to find out. Deno.readDir()
or synchronously Deno.readDirSync()
. This
method, like many of the following, is similar to NodeJS.
Creating new directories
As we are used to from the terminal/console, we can create new directories with
Deno. The command is Deno.mkdir()
.
We have to keep in mind that the system does not check if the directory already
exists when the command is being executed. If it does, Deno.mkdir("/my/path")
will return an error. So we need to check that the directory exists first. We
can do this with Deno.stat("/my/path")
. Let's look at this in an example.
const path = "my-dir";
try {
const stat = await Deno.stat(path);
if (!stat.isDirectory) {
throw new Error(`Path exists but is not a directory: ${path}`);
}
console.log(`Path exists and is a directory: ${path}`);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log(`Path does not exist. We can create it: ${path}`);
await Deno.mkdir(path);
}
}
We can also create subfolders in one step. To do this, we pass a second
parameter to the Deno.mkdir()
functionβs options with { recursive: true }
.
const path = "my-dir/sub-dir";
try {
const stat = await Deno.stat(path);
if (!stat.isDirectory) {
throw new Error(`Path exists but is not a directory: ${path}`);
}
console.log(`Path exists and is a directory: ${path}`);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.log(`Path does not exist. We can create it: ${path}`);
await Deno.mkdir(path, { recursive: true });
}
}
Walk directories
For example, if we want to build an application that displays the file system
and its contents, we have the option of traversing the file system from a
starting point. This method is provided by the @std/fs
module from the
standard library. Using an additional function from the @std/path
library, we
can create a kind of tree
application.
import { walk } from "jsr:@std/fs/walk";
import { relative, SEPARATOR } from "jsr:@std/path";
const path = ".";
for await (const entry of walk(path, { includeDirs: true })) {
const relativePath = relative(path, entry.path);
const depth = relativePath.split(SEPARATOR).length - 1;
const indent = "-->".repeat(depth);
const prefix = entry.isDirectory ? "π " : "π ";
console.log(`${prefix}${indent} ${entry.name}`);
}
An example output would be the following, which shows my test project:
β deno run -A walk.ts
π .
π .zed
π --> settings.json
π main.ts
π deno.json
π walk.ts
π deno.lock
π .env
π flags.ts
π my-dir
π --> sub-dir
π Further Documentation
Creating, reading and deleting files
Now we have looked at directories, interacted with them and created them. Of
course, not only directories can be created, but files of all kinds. There are
several methods in the Deno namespace and in the @std/fs
library that we can
use to do this.
The most important ones are described below.
Creating a new file
To create a new file, we can differentiate between a text file and a general file.
Creating text files
Deno offers us a simpler method for the former. We can use
Deno.writeTextFile()
to pass strings directly as input to the text file. We
have an asynchronous promise-based method on the one hand, and a synchronous
variant on the other. We have to wait for the former with an `await', but the
following code can continue to run and will not block the application.
// async
await Deno.writeTextFile("hello.txt", "Hello, Deno!");
// sync
Deno.writeTextFileSync("hello.txt", "Hello, Deno!");
First we have the filename we want to assign. Then we can pass any string to be written to the text file. If the file already exists, it will be overwritten.
Of course, we don't always want to create a new file and overwrite existing data. To do this, we can use the method options (3rd parameter) to append our text to a file.
await Deno.writeTextFile("hello.txt", "Hello, Deno!", { append: true });
Creating arbitrary files
It is possible to use the Deno.create()
method to create an empty file and
optionally write bytes to it afterwards. Deno will then return a Deno.FsFile
object that we can work with (read, write, truncate, etc.).
const f = await Deno.create("hello.txt");
const stats = await f.stat();
console.log(stats.isFile); // Result: true
To write bytes to this file, we need a Writer (and a TextEncoder if we want to write text). Let's have a look at what this looks like in code.
const file = await Deno.create("hello.txt");
// check the file size
const statsBefore = await file.stat();
console.log(statsBefore.size); // Result: 0
const writer = file.writable.getWriter();
await writer.write(new TextEncoder().encode("Hello Deno!"));
// check the size now
const statsAfter = await file.stat();
console.log(statsAfter.size); // Result: 11
// close the file so we don't leak
writer.close(); // or f.close() - writer.close() also closes the file
A simple workflow is provided by Deno.writeFile()
. This allows us to
significantly shorten the previous code.
await Deno.writeFile(
"hello.txt",
new TextEncoder().encode("Hello Deno!"),
);
const stats = await Deno.stat("hello.txt");
console.log(stats.size); // Result: 11
Reading the contents of a file
When we want to read a file, we can differentiate between a text file and a general file, just like when we create a file. For text files, there is a method that returns a string directly. If we want to read the file we created earlier, we can do this as follows.
const content = await Deno.readTextFile("hello.txt");
console.log(content); // Hello Deno!
However, if we want to load an arbitrary file into memory, we can use the
Deno.readFile()
method. This will return a byte array that we can work with.
const bytes = await Deno.readFile("hello.txt");
console.log(new TextDecoder().decode(bytes));
We don't have to worry about closing them with either method. Deno does that for us.
As with Deno.create()
, we can open files to work with. For this we have the
Deno.open()
method. This will open the file and we can then work with it. Be
it reading or writing. As with Deno.create()
we get a Deno.FsFile
object.
Removing files or directories
Deleting files and directories is simpler. Deno does not differentiate between
the two types. With Deno.remove()
or Deno.removeSync()
we can remove both
types.
// remove a file
await Deno.remove("hello.txt");
// remove an empty directory
await Deno.remove("my-dir");
// remove a non-empty directory
await Deno.remove("my-dir", { recursive: true });
When we try to delete a non-empty directory, we get an error message from Deno
error: Uncaught (in promise) error: Directory not empty (os error 66): remove "my-dir"
.
So we need to set the { recursive: true }
option deliberately to remove
everything in that directory.
Renaming and moving files
Sometimes we just want to move or rename files. For this we have the
Deno.rename()
function, which behaves similarly to the mv
we may know from
the terminal. The function is quickly explained. The first parameter is the
current location and the second is the future location.
await Deno.rename("hello.txt", "./public/hello.txt");
It is important to note that the source location must be readable and the destination location must be writable for our application.
Temporary files
Temporary files are great for large amounts of data that you don't want to keep
in memory. If they are to be used later in the program, we can use them to store
data temporarily. Or, if you want to send them somewhere else, you can simply
remove them at the end of the program. By default, these files are created in
the operating system's temporary folder (such as /tmp
or /var/folders
). To
get the current temporary directory, we can use one of the following two methods
(preferably the first)
// method 1 via NodeJS compatibility layer
import { tmpdir } from "node:os";
const tempPath = tmpdir();
console.log(tempPath);
// method 2 using Deno environment variables
const tempDir = Deno.env.get("TMPDIR") ||
Deno.env.get("TMP") ||
Deno.env.get("TEMP");
console.log(tempDir);
For the creation of temporary files or directories we then have the functions
Deno.makeTempDir()
and Deno.makeTempFile()
.
const tempPath = await Deno.makeTempDir();
console.log(tempPath);
const tempFile = await Deno.makeTempFile();
console.log(tempFile);
Both functions return the path to the directory or file created, so we can interact with it.
The options that are optional as parameters help us to better identify the
created files, as they have gibberish looking names. For example
/var/folders/j_/g0c8_fyd0zq3vg76xr8xcyhc0000gn/T/fc8e227ff388a6c2
. To make the
files or directories more identifiable, we can use either {prefix: ""}
or
{suffix: ""}
as an option (or both).
With the following code we get results like
/var/folders/j_/g0c8_fyd0zq3vg76xr8xcyhc0000gn/T/temp_f0bba33b51e5bb67.txt
.
const tempFile = await Deno.makeTempFile({ prefix: "temp_", suffix: ".txt" });
console.log(tempFile);
That way we know that it's probably a text file we're working with. This makes it easier to find and work with.
π Further Documentation
- Writing files - Deno by Example
- Reading files - Deno by Example
- Deleting files - Deno by Example
- Moving/Renaming files - Deno by Example
- Temporary files & directories - Deno by Example
Manage files (permissions, directories, statistics)
See statistics about files and directories
We can use Deno functions to get all sorts of information about our environment.
For example, we can get statistics about files and directories. The
Deno.stat()
function is available for this purpose.
In the following we will look at a slimmed down implementation of the terminal
application ls
. We want to give the user information about the current
directory. The output should return the available files, with optional CLI
arguments for file size and last modified date.
In the following case, we create a file called ls.ts
and insert the following
code.
import { parseArgs } from "jsr:@std/cli/parse-args";
import { ensureDir } from "jsr:@std/fs";
interface FileInfo {
name: string;
size: number;
mtime: Date | null;
isDirectory: boolean;
isFile: boolean;
isSymlink: boolean;
}
interface Options {
all: boolean;
long: boolean;
human: boolean;
}
/**
* Convert a file size in bytes to a human-readable string.
* @param sizeBytes The size in bytes.
* @param human Whether to use human-readable units.
* @returns The size as a string.
*/
function convertSize(sizeBytes: number, human = false): string {
if (!human) return sizeBytes.toString();
const units = ["B", "K", "M", "G", "T"];
let size = sizeBytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)}${units[unitIndex]}`;
}
/**
* Get file information for a directory entry.
* @param path The path to the directory.
* @param entry The directory entry.
* @returns The file information.
*/
async function getFileInfo(
path: string,
entry: Deno.DirEntry,
): Promise<FileInfo> {
const fullPath = `${path}/${entry.name}`;
const stat = await Deno.stat(fullPath);
return {
name: entry.name,
size: stat.size,
mtime: stat.mtime,
isDirectory: entry.isDirectory,
isFile: entry.isFile,
isSymlink: entry.isSymlink,
};
}
/**
* List the contents of a directory.
* @param path The path to the directory.
* @param options The options to use.
*/
async function listDirectory(path: string, options: Options) {
const entries: Deno.DirEntry[] = [];
for await (const entry of Deno.readDir(path)) {
if (!options.all && entry.name.startsWith(".")) continue;
entries.push(entry);
}
// Sort entries
entries.sort((a, b) => a.name.localeCompare(b.name));
if (options.long) {
// Add . and .. if showing all
if (options.all) {
const currentDir = await Deno.stat(".");
const parentDir = await Deno.stat("..");
console.log(formatLongEntry({
name: ".",
size: currentDir.size,
mtime: currentDir.mtime,
isDirectory: true,
isFile: false,
isSymlink: false,
}, options));
console.log(formatLongEntry({
name: "..",
size: parentDir.size,
mtime: parentDir.mtime,
isDirectory: true,
isFile: false,
isSymlink: false,
}, options));
}
// Process and display each entry
for (const entry of entries) {
const info = await getFileInfo(path, entry);
console.log(formatLongEntry(info, options));
}
return;
}
// Simple listing
for (const entry of entries) {
console.log(entry.name);
}
return;
}
/**
* Format a file information object for a long listing.
* @param info The file information.
* @param options The options to use.
* @returns The formatted string.
*/
function formatLongEntry(info: FileInfo, options: Options): string {
const mtime = info.mtime
? info.mtime.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "???";
const size = convertSize(info.size, options.human);
return `${size.padStart(6)} ${mtime} ${info.name}`;
}
async function main() {
const args = parseArgs(Deno.args, {
boolean: ["a", "l", "h"],
alias: {
a: "all",
l: "long",
h: "human",
},
});
const options: Options = {
all: args.a || false,
long: args.l || false,
human: args.h || false,
};
const path = args._[0]?.toString() || ".";
try {
await ensureDir(path);
await listDirectory(path, options);
} catch (error) {
console.error(`Error: ${error.message}`);
Deno.exit(1);
}
}
if (import.meta.main) {
main();
}
We can run this application with
deno run --allow-read ls.ts <dir> <parameter>
. If we run it without specifying
any directories or parameters, we will get the information from the current
directory.
β deno run --allow-read ls.ts
deno.json
deno.lock
ls.ts
It gets interesting when we use one (or more) of our parameters. We have the
parameters -a
, -l
and -h
. The last one works in combination with -l
.
When used, we get the following output from our application.
β deno run --allow-read ls.ts -alh
160.0B Nov 17, 03:35 PM .
96.0B Oct 16, 05:37 PM ..
118.0B Nov 17, 03:36 PM deno.json
675.0B Nov 17, 03:36 PM deno.lock
3.7K Nov 17, 03:37 PM ls.ts
As a result, we have built our own ls
application in a simple way.
π Further Documentation
Concluding "Deno and the file system"
In this chapter we learned about Deno's file system operations. We covered how
to interact with directories, including checking their contents, creating new
ones, and traversing through them using the walk
function. We then explored
file operations such as creating, reading, and deleting files, as well as
working with temporary files. The chapter demonstrated both synchronous and
asynchronous methods, showing how to handle file permissions and statistics.
Throughout the examples, we saw how Deno provides a secure approach to file
system operations by requiring explicit permissions through runtime flags like
--allow-read
and --allow-write
. We concluded with a practical implementation
of an ls
-like command-line tool, which brought together many of the concepts
we learned.