An Introduction to Deno: Is It Better than Node.js? | AppSignal Blog
Deno is a JavaScript and TypeScript runtime similar to
Node.js, built on Rust and the V8 JavaScript engine.
It was created by Ryan Dahl, the original inventor of Node.js, to counter
mistakes he made when he originally designed and released Node.js back in 2009.
Ryan’s regrets about Node.js are well documented in his famous
’10 Things I Regret About Node.js’ talk at JSConf EU in 2018.
To summarize, he bemoaned the lack of attention to security, module resolution
through node_modules
, various deviations from how browser’s worked, amongst
other things, and he set out to fix all these mistakes in Deno.
In this article, we’ll discuss why Deno was created and its advantages and
disadvantages compared to Node.js. It’ll also give a practical overview of
Deno’s quirks and features so that you can decide if it’s a good fit for your
next project.
Nội Dung Chính
Installing Deno
Deno is distributed as a single, self-contained binary without any dependencies.
You can install Deno in various
ways, depending
on your operating system. The simplest method involves downloading and executing
a shell script as shown below:
bash
# Linux and macOS
$
curl
-fsSL
https://deno.land/x/install/install.sh
|
sh
# Windows PowerShell
$
iwr
https://deno.land/x/install/install.ps1
-useb
|
iex
Once you’ve executed the appropriate command for your operating system, the Deno
CLI binary will be downloaded to your computer. You may be required to add the binary location to your PATH
, depending on the installation method you
chose.
You can do this in Bash by adding the lines below to your
$HOME/bash_profile
file. You may need to start a new shell session for the
changes to take effect.
bash
export
DENO_INSTALL
=
"
$HOME
/.deno"
export
PATH
=
"
$DENO_INSTALL
/bin:
$PATH
"
To verify the installed version of Deno, run the command below. It
should print the Deno version to the console if the CLI was downloaded
successfully and added to your PATH
.
bash
$
deno
--version
deno
1.14.2
(
release,
x86_64-unknown-linux-gnu
)
v8
9.4.146.16
typescript
4.4.2
If you have an outdated version of Deno, upgrading to the latest release can be
done through the upgrade
subcommand:
bash
$
deno
upgrade
Looking
up
latest
version
Found
latest
version
1.14.2
Checking
https://github.com/denoland/deno/releases/download/v1.
14.2
/deno-x86_64-unknown-linux-gnu.zip
31.5
MiB /
31.5
MiB (
100.0
%)
Deno
is
upgrading
to
version
1.14.2
Archive:
/tmp/.tmpfdtMXE/deno.zip
inflating:
deno
Upgraded
successfully
Go ahead and write a customary hello world program to verify that everything
works correctly. You can create a directory for your Deno programs and place
the following code in an index.ts
file at the directory’s root.
typescript
function
hello
(
str
:
string
) {
return
`Hello ${
str
}!`
;
}
console.
log
(
hello
(
"Deno"
));
Save and execute the file by providing the filename as an argument to the
run
subcommand. If the text “Hello Deno!” outputs, it means that you have
installed and set up Deno correctly.
bash
$
deno
run
index.ts
Check
file:///home/ayo/dev/deno/index.ts
Hello
Deno!
To find out about other features and options provided by the Deno CLI, use the
--help
flag:
bash
$
deno
--help
↓ Article continues below
Is your app broken or slow?
AppSignal lets you know.
Monitoring by AppSignal →
Deno’s First-class TypeScript Support
One of the big selling points of Deno over Node.js is its first-class support
for TypeScript.
As you’ve already seen, you don’t need to do anything
besides install the Deno CLI for it to work. Like its predecessor, Deno uses
the V8 runtime engine under the hood to parse and execute JavaScript code, but
it also includes the TypeScript
compiler in its executable to achieve
TypeScript support.
Under the hood, TypeScript code is checked and compiled. The resulting
JavaScript code is cached in a directory on your filesystem, ready
to be executed again without being compiled from scratch. You can use deno info
to inspect the location of the cache directory and other directories
containing Deno-managed files.
Deno does not require any configuration to work with TypeScript, but you can
provide one if you want to tweak how the TypeScript compiler parses the code.
You can provide a JSON file to specify the TypeScript compiler options.
Although tsconfig.json
is the convention when using the standalone tsc
compiler, the Deno team recommends using deno.json
because other Deno-specific
configuration options can be placed there.
Note that Deno doesn’t support all TypeScript compiler options. A full
list
of the available options, along with their default values, are presented in the
Deno documentation. Here’s a sample configuration file for Deno:
json
{
"compilerOptions"
: {
"checkJs"
:
true
,
"noImplicitReturns"
:
true
,
"noUnusedLocals"
:
true
,
"noUnusedParameters"
:
true
,
"noUncheckedIndexedAccess"
:
true
}
}
At the time of writing, Deno does not automatically detect a deno.json
file
so it must be specified via the --config
flag. However, this feature is
planned for a future release.
bash
$
deno
run
--config
deno.json
index.ts
When the Deno CLI encounters a type error, it halts the compilation of the
script and terminates with a non-zero exit code. You can bypass the error by:
- using
//@ts-ignore
or//@ts-expect-error
at the point where the error
occurred or // @ts-nocheck
at the beginning of the file to ignore all errors
in a file.
Deno also provides a --no-check
flag to disable type checking
altogether. This helps prevent the TypeScript compiler from slowing
you down when iterating quickly on a problem.
bash
$
deno
run
--no-check
index.ts
Permissions in Deno
Deno prides itself on being a secure runtime for JavaScript and TypeScript. Part
of the way it maintains security is through its permissions
system. To
demonstrate how permissions work in Deno, add the below script to your
index.ts
file. It’s a script that fetches the latest global Covid-19
statistics from disease.sh.
typescript
async
function
getCovidStats
() {
try
{
const
response
=
await
fetch
(
"https://disease.sh/v3/covid-19/all"
);
const
data
=
await
response.
json
();
console.
table
(data);
}
catch
(err) {
console.
error
(err);
}
}
getCovidStats
();
When you attempt to execute the script, it should display a PermissionDenied
error:
bash
$
deno
run
index.ts
PermissionDenied:
Requires
net
access
to
"disease.sh",
run
again
with
the
--allow-net
flag
The error message above indicates that the script hasn’t been granted network access. It suggests including the --allow-net
flag in the command to
grant access.
bash
$
deno
run
--allow-net
index.ts
┌────────────────────────┬───────────────┐
│
(
idx
)
│
Values
│
├────────────────────────┼───────────────┤
│
updated
│
1633335683059
│
│
cases
│
235736138
│
│
todayCases
│
32766
│
│
deaths
│
4816283
│
│
todayDeaths
│
670
│
│
recovered
│
212616434
│
│
todayRecovered
│
51546
│
│
active
│
18303421
│
│
critical
│
86856
│
│
casesPerOneMillion
│
30243
│
│
deathsPerOneMillion
│
617.9
│
│
tests
│
3716763329
│
│
testsPerOneMillion
│
473234.63
│
│
population
│
7853954694
│
│
oneCasePerPeople
│
0
│
│
oneDeathPerPeople
│
0
│
│
oneTestPerPeople
│
0
│
│
activePerOneMillion
│
2330.47
│
│
recoveredPerOneMillion
│
27071.26
│
│
criticalPerOneMillion
│
11.06
│
│
affectedCountries
│
223
│
└────────────────────────┴───────────────┘
Instead of granting blanket approval for the script to access all websites (as shown
above), you can provide an allowlist of comma-separated hostnames or IP
addresses as an argument to --allow-net
so that only the specified websites
are accessible by the script. If the script tries to connect to a domain that
is not in the allowlist, Deno will prevent it from connecting, and the script
execution will fail.
bash
$
deno
run
--allow-net=
'disease.sh'
index.ts
This feature is one of Deno’s improvements over Node.js where
any script can access any resource over the network. Similar permissions also
exist for reading from and writing to the filesystem. If a script needs to
perform either task, you need to specify the --allow-read
and --allow-write
permissions, respectively. Both flags allow you to set the specific directories
accessible to a script so that other parts of the filesystem are safe
from tampering. Deno also provides an --allow-all
flag that enables all
security-sensitive functions for a script, if so desired.
Deno’s Compatibility With Browser APIs
One of Deno’s main goals
is to be compatible with web browsers, where possible. This is reflected in its
use of web platform APIs instead of creating a Deno-specific API for certain
operations. For example, we saw the Fetch
API in action in
the previous section. This is the exact Fetch API used in browsers,
with a few deviations where necessary to account for the unique security model
in Deno (and these changes are mostly inconsequential).
There’s a
list of all implemented
browser APIs in Deno’s online documentation.
Dependency Management in Deno
The way Deno manages dependencies is probably the most obvious way it
diverges significantly from Node.js.
Node.js uses a package manager
like npm
or yarn
to download third-party packages from the npm
registry into a node_modules
directory and a
package.json
file to keep track of a project’s dependencies. Deno does away
with those mechanisms in favor of a more browser-centric way of using third-party packages: URLs.
Here’s an example that uses Oak, a web
application framework for Deno, to create a basic web server:
typescript
import
{ Application }
from
"https://deno.land/x/oak/mod.ts"
;
const
app
=
new
Application
();
app.
use
((
ctx
)
=>
{
ctx.response.body
=
"Hello Deno!"
;
});
app.
addEventListener
(
"listen"
, ({
hostname
,
port
,
secure
})
=>
{
console.
log
(
`Listening on: http://localhost:${
port
}`
);
});
await
app.
listen
({ port:
8000
});
Deno uses ES modules, the same module system used in web browsers. A
module can be imported from an absolute or relative path, as long as the
referenced script exports methods or other values. It’s worth noting that the
file extension must always be present, regardless of whether you import
from an absolute or relative path.
While you can import modules from any URL, many third-party modules
specifically built for Deno are cached through
deno.land/x. Each time a new version of a module is
released, it is automatically cached at that location and made immutable so that
the contents of a specific version of a module remain unchangeable.
Suppose you run the code in the previous snippet. In that case, it will download the module and all
its dependencies and cache them locally in the directory specified by the
DENO_DIR
environmental variable (the default should be $HOME/.cache/deno
).
The next time the program runs, there will be no downloads since all the
dependencies have been cached locally. This is similar to how the Go
module system works.
bash
$
deno
run
--allow-net
index.ts
Download
https://deno.land/x/oak/mod.ts
Warning
Implicitly
using
latest
version
(
v9.0.1
)
for
https://deno.land/x/oak/mod.ts
Download
https://deno.land/x/oak@v9.
0.1
/mod.ts
.
.
.
For production applications, the creators of Deno
recommend
vendoring your dependencies by checking them into source control to ensure
continued availability (even if the source of the module is unavailable, for
whatever reason). Point the DENO_DIR
environmental variable
to a local directory in your project (such as vendor
), which you can commit to
Git.
For example, the command below will download all your script’s dependencies into
a vendor
directory in your project. You can subsequently commit the folder to pull it down all at once in your production server. You’ll also
need to set the DENO_DIR
variable to read from the vendor
directory on the server, instead of downloading them all over again.
bash
$
DENO_DIR=
$PWD
/vendor
deno
cache
index.ts
# Linux and macOS
$
$env
:DENO_DIR="$(
get-location
)\vendor"
;
deno
cache
index.ts
# Windows PowerShell
Deno also supports the concept of versioning your dependencies to ensure
reproducible builds. At the moment, we’ve imported Oak from
https://deno.land/x/oak/mod.ts
. This always downloads the latest version, which
could become incompatible with your program in the future. It also causes Deno
to produce a warning when you download the module for the first time:
bash
Warning
Implicitly
using
latest
version
(
v9.0.1
)
for
https://deno.land/x/oak/mod.ts
It is considered best practice to reference a specific release as follows:
bash
import
{
Application
}
from
'https://deno.land/x/[email protected]/mod.ts'
;
If you’re referencing a module in many files in your codebase, upgrading it may
become tedious since you have to update the URL in many places. To circumvent
this issue, the Deno team
recommends
importing your external dependencies in a centralized deps.ts
file and
then re-exporting them. Here’s a sample deps.ts
file that exports what we need
from the Oak library.
typescript
export
{ Application, Router }
from
"https://deno.land/x/[email protected]/mod.ts"
;
Then in your application code, you can import them as follows:
typescript
import
{ Application, Router }
from
"./deps.ts"
;
At this point, updating a module becomes a simple matter of changing the URL in
the deps.ts
file to point to the new version.
The Deno Standard Library
Deno provides a standard libary (stdlib) that aims to
be a loose port of Go’s standard library. The modules
contained in the standard library are audited by the Deno team and updated with
each release of Deno. The intention behind providing a stdlib is to allow
you to create useful web applications right away, without resorting to any third-party packages (as is the norm in the Node.js ecosystem).
Some examples of standard library modules you might find helpful include:
- HTTP: An HTTP client and server
implementation for Deno. - Fmt: Includes helpers for printing formatted output.
- Testing: Provides basic utilities for
testing and benchmarking your code. - FS: Has helpers for manipulating the
filesystem. - Encoding: Provides helpers to
deal with various file formats, such as XML, CSV, base64, YAML, binary, and
more. - Node: Has a compatibility layer for
the Node.js standard library.
Here’s an example (taken from Deno’s official
docs)
that utilizes the http
module in Deno’s stdlib to create a basic web server:
typescript
import
{ listenAndServe }
from
"https://deno.land/[email protected]/http/server.ts"
;
const
addr
=
":8080"
;
const
handler
=
(
request
:
Request
)
:
Response
=>
{
let
body
=
"Your user-agent is:
\n\n
"
;
body
+=
request.headers.
get
(
"user-agent"
)
||
"Unknown"
;
return
new
Response
(body, { status:
200
});
};
console.
log
(
`HTTP webserver running. Access it at: http://localhost:8080/`
);
await
listenAndServe
(addr, handler);
Start the server through the command below:
bash
$
deno
run
--allow-net
index.ts
HTTP
webserver
running.
Access
it
at:
http://localhost:
8080
/
In a different terminal, access the running server through the following
command:
bash
$
curl
http://localhost:
8080
Your
user-agent
is:
curl/7.68.0
Note that the modules in the standard library are currently tagged as unstable
(as reflected in the version number). This means you should not rely
on them just yet for a serious production application.
Using NPM Packages in Deno
It cannot be denied that one of the major reasons why Node.js has been so
successful is the large number of packages that can be downloaded and utilized
in a project. If you’re considering switching to Deno, you may be wondering if
you’d have to give up all the NPM packages you know and love.
The short answer is: no. While you may not be able to utilize some NPM packages
in Deno if they rely on Node.js APIs (especially if the specific APIs are not
supported in Deno’s Node.js compatibility layer),
many NPM packages can be utilized in Deno through CDNs like
esm.sh and skypack.dev. Both these CDNs
provide NPM packages as ES Modules that can be subsequently consumed in
a Deno script even if the author of the package did not design it to target Deno
specifically.
Here’s an example that imports the dayjs NPM
package from Skypack in a Deno script:
typescript
import
dayjs
from
"https://cdn.skypack.dev/[email protected]"
;
console.
log
(
`Today is: ${
dayjs
().
format
(
"MMMM DD, YYYY"
)
}`
);
bash
$
deno
run
index.ts
Today
is:
October
0
5
,
2021
To ensure that Deno can discover the types associated with a package,
ensure you add the ?dts
suffix at the end of the package URL when using
Skypack’s CDN. This causes Skypack to set a X-TypeScript-Types
header so that
Deno can automatically discover the types associated with a package. Esm.sh
includes this header by default, but you can opt-out by adding the ?no-check
suffix at the end of the package URL.
typescript
import
dayjs
from
"https://cdn.skypack.dev/[email protected]?dts"
;
Deno Tooling
The Deno CLI comes with several valuable tools that make the developer experience
much more pleasant. Like Node.js, it comes with a REPL (Read Evaluate Print
Loop), which you can access with deno repl
.
bash
$
deno
repl
Deno
1.14.2
exit
using
ctrl+d
or
close
()
>
2
+
2
4
It also has a built-in file
watcher
that can be used with several of its subcommands. For example, you can configure
deno run
to automatically rebuild and restart a program once a file is
changed by using the --watch
flag. In Node.js, this functionality is generally
achieved through some third-party package such as
nodemon.
bash
$
deno
run
--allow-net
--watch
index.ts
HTTP
webserver
running.
Access
it
at:
http://localhost:
8080
/
Watcher
File
change
detected!
Restarting!
HTTP
webserver
running.
Access
it
at:
http://localhost:
8080
/
With Deno 1.6, you can compile scripts
into self-contained executables that do not require Deno to be installed
through the compile
subcommand (you can use pkg to do the same in Node.js). You can also generate executables for other
platforms (cross
compilation)
through the --target
flag. When compiling a script, you must specify the
permissions needed for it to run.
bash
$
deno
compile
--allow-net
--output
server
index.ts
$
./server
HTTP
webserver
running.
Access
it
at:
http://localhost:
8080
/
Note that the binaries produced through this process are quite huge. In
my testing, deno compile
produced an 83MB binary for a simple “Hello world”
program. However, the Deno team is currently working on a way to reduce the file
sizes to be a lot more manageable.
Another way to distribute a Deno program is to package it into a single
JavaScript file through the bundle
subcommand. This file contains the source
code of the program and all its dependencies, and it can be executed through
deno run
as shown below.
bash
$
deno
bundle
index.ts
index.bundle.js
Check
file:///home/ayo/dev/demo/deno/index.js
Bundle
file:///home/ayo/dev/demo/deno/index.js
Emit
"index.bundle.js"
(7.39KB)
$
deno
run
--allow-net
index.bundle.js
HTTP
webserver
running.
Access
it
at:
http://localhost:
8080
/
Two additional great tools that Deno ships with are the built-in
linter (deno lint
) and
formatter (deno fmt
). In
the Node.js ecosystem, linting and formatting code are typically handled with
ESLint and Prettier, respectively.
When using Deno, you no longer need to install anything or write configuration
files to get linting and formatting for JavaScript, TypeScript, and other
supported file
formats.
Unit Testing in Deno
Support for unit testing is built
into Deno for both JavaScript and TypeScript code. When you run deno test
, it
automatically detects any files that end with _test.ts
or .test.ts
(also
supports other file extensions) and executes any defined tests therein.
To write your first test, create an index_test.ts
file and populate it with
the following code:
typescript
import
{ assertEquals }
from
"https://deno.land/[email protected]/testing/asserts.ts"
;
Deno.
test
(
"Multiply two numbers"
, ()
=>
{
const
ans
=
2
*
2
;
assertEquals
(ans,
4
);
});
Deno provides the Deno.test
method for creating a unit test. It takes the name
of the test as its first argument. Its second argument is the function executed when
the test runs.
There is a second style that takes in an object
instead of two arguments. It supports other
properties
aside from the test name and function to configure if or how
the test should run.
typescript
Deno.
test
({
name:
"Multiply two numbers"
,
fn
() {
const
ans
=
2
*
2
;
assertEquals
(ans,
4
);
},
});
The assertEquals()
method comes from the testing
module in the standard
library, and it provides a way to easily check the equality of two values.
Go ahead and run the test:
bash
$
deno
test
test
Multiply
two
numbers
...
ok
(8ms)
test
result:
ok.
1
passed
;
0
failed
;
0
ignored
;
0
measured
;
0
filtered
out
(37ms)
The Deno Language Server
One of the major considerations for choosing a programming language or
environment is its integration with editors and IDEs. In Deno 1.6, a built-in
language server (deno lsp
) was added to the runtime to provide
features such as:
- autocompletion
- go-to-definition
- linting and formatting
integration
As well as other language smarts for any editor that supports the
Language Server
Protocol (LSP). You can
learn more about setting up Deno support in your editor in Deno’s online
docs.
Wrapping up: Should I Choose Deno over Node.js?
In this article, we’ve considered many aspects of the Deno runtime, and ways in which it’s an upgrade over Node.js.
There’s a lot more to say about
Deno and its ecosystem, but this should hopefully be a helpful introduction for
Node.js developers considering Deno for a new project. The
lesser availability of third-party packages for Deno is an obvious aspect where it falls
short, as is the fact that it’s not as battle-tested as Node.js in the real
world due to its young age (Deno 1.0 was released in May 2020).
Comparing the
performance between
Node.js and Deno shows that they’re within the same ballpark in most cases,
although there are a few
scenarios
where Node.js exhibits far superior performance. The measured disparities
are bound to improve as Deno becomes more mature.
When deciding between Node.js and Deno, it’s also important to keep in mind that
some of the benefits that Deno provides can also be brought to Node.js with the
help of third-party packages. So if there are only one or two things that you
admire about Deno, chances are you’ll be able to achieve a similar result in
Node.js, though not as seamlessly.
Thanks for reading, and happy coding!
P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.
P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.