During the last 10 years, Node.js has become a big player in the backend framework market, powering several large scale applications across the globe. Meanwhile, JavaScript has also evolved greatly, not only because of the efforts of its development team, but also based on community feedback. However, integrating some of these new language features into a 10-year-old framework is not really straightforward, and has a high level of complexity.
Therefore we could say that Node.js’ architecture hasn’t evolved as fast as the language. As a basic example, Node.js is still based on callbacks, while there are far better ways to deal with asynchronicity in modern JavaScript. This is something that its creator, Ryan Dahl, has acknowledged in the past few years, and it has moved him to work on a new framework that addresses some of these issues. It is called Deno, and in the following article, we would like to explore some of its concepts to determine if it will render Node.js obsolete.
What is Deno?
First of all, you should know that Deno is not a fork of Node.js. It's a modern runtime for JavaScript and TypeScript, implemented from scratch by Ryan Dahl in 2018. It was built on V8 just like Node.js, but Deno was written with Rust and Tokio. The runtime was designed with TypeScript in mind, and for that reason Deno supports TypeScript without extra configurations or tooling.
Deno was built to improve security and help increase productivity in developers using the latest JavaScript features. At the time this article was written, Deno's version is 1.4.0. It's a stable release, so it's a good time to go over its main features and learn how you can use them for your application.
Features
TypeScript out of the box:
TypeScript is powerful. This superset of JavaScript allows us to use types, interfaces, classes, inheritance, modules, generics, and other awesome things. However, it can be a bit tricky when using it with Node.js, because we need to install a module for TypeScript support and some tools to transpile the code. It also requires some additional configurations through tsconfig.json. However, after all of this setup, the JavaScript files that get compiled from TypeScript work pretty well with Node.js.
Deno offers native support for TypeScript at its 3.9 version. For it to run nothing else needs to be installed, and no compilation step is needed since Deno transpiles the code behind the scenes. It is also possible to run the code with a custom tsconfig.json
file to customize how Deno compiles your code.
# Using a custom tsconfig.json file
deno run -c tsconfig.json my-application.ts
URL imports:
Node.js projects have a package.json
file that contains relevant information for your project. It also holds the dependency list that you’ll be using. Deno handles this part in a completely different way. The package.json
file is not used anymore in favor of ES Modules
. In order to use modules in a Deno project, you will need to reference each module with its URL or file path.
# Import module server
import {serve} from “https://deno.land/std/http/server.ts”
When the application is executed for the first time, Deno downloads and caches all modules in a global cache. It is possible to store them in a custom directory using the $DENO_DIR
environment variable. With this approach, Deno decentralizes the modules and your project will not have a large node_modules
folder.
To keep module versions locked, it is possible to create a lock file with the --lock
and --lock-write
flags.
# Create/update the lock file "lock.json"
deno cache --lock=lock.json --lock-write src/my-application.ts
Secure by Default:
Deno has some security measures in place to disallow potentially dangerous operations. By default, the code is executed in a secure sandbox, so it is not possible to access the network, file system, or environment unless you explicitly allow it. You can do this by adding flags when running the application. These are enabled by default in Node.js, which makes it insecure in some cases.
As a quick overview of what can be enabled we have the following flags:
# Enable environment access with Deno.env.get
deno run --allow-env my-application.ts
#Enable high resolution time measurement (used for profiling)
deno run --allow-hrtime my-application.ts
#Enable network access
# This is used in cases where we want to fetch from external servers
# or when we want to expose a port from our server
deno run --allow-net=https://example.com my-application.ts
# Enable plugin usage
deno run --allow-plugin my-application.ts
# Enable filesystem access
# To read a file or directory with Deno.open, or write to a file
# or directory with Deno.writeFile
deno run --allow-read=awesome.txt my-application.ts
deno run --allow-write=awesome.txt my-application.ts
# Enable subprocess execution with Deno.run
deno run --allow-run my-application.ts
# Disable all security checks
deno run --allow-all my-application.ts
Built-in utilities:
In Deno, we have some nice tools available out of the box. This means that we don’t need to install any additional libraries for some common development tasks. At a glance, these utilities are:
Debugger: Like Node, Deno supports the V8 Inspector Protocol, which means that it’s possible to debug the program in any client that supports it. This consists mainly of two commands:
# Debugging flags
# --inspect : Allows debugger attachment at any point
# --inspect-brk : Pause execution on first line
# Usage:
deno run --inspect-brk
From this point, all you have to do is open your client and the program will stop on the first line, allowing you to set up breakpoints where you need to. For VSCode you can add the entry point and arguments to your launch.json
to enable it.
Formatter: In Node.js applications, it’s common to add a linter tool (typically ESLint) with a formatter like Prettier so the code is standardized. Deno has this feature built into it. You can access it by simply running deno fmt
deno fmt #Formats everything in the current tree
deno fmt file1.ts #Formats a single file
deno fmt --check # Checks if files are formatted correctly
Bundler: Bundling is a task that is currently done with webpack, gulp or Grunt in Node.js applications. We can use Deno’s bundler when we want to pack a module together with its dependencies and this will generate a single module we can reference from other files. These bundles can also be loaded in the web browser.
deno bundle https://foo.bar/test.ts test.bundle.js
After that, we can import it from another JavaScript file or using a <script>
tag in our HTML with the type=”module”
property.
Dependency inspector: We can display a tree structure of our dependencies using the deno info
command, followed by the URL we want to inspect. This is similar to npm ls
in Node
deno info https://deno.land/std/uuid/test.ts
local: /Users/foo/Library/Caches/deno/deps/https/deno.land/997789467b3621b5d93c6b18bf8b275f35057b24f934c2508e3d1ef52cd51644
type: TypeScript
compiled: /Users/foo/Library/Caches/deno/gen/https/deno.land/std/uuid/test.ts.js
map: /Users/foo/Library/Caches/deno/gen/https/deno.land/std/uuid/test.ts.js.map
deps:
https://deno.land/std/uuid/test.ts
├─┬ https://deno.land/std/uuid/tests/isNil.ts
│ ├─┬ https://deno.land/std/testing/asserts.ts
│ │ ├── https://deno.land/std/fmt/colors.ts
│ │ └── https://deno.land/std/testing/diff.ts
│ └─┬ https://deno.land/std/uuid/mod.ts
│ ├─┬ https://deno.land/std/uuid/v1.ts
.
.
.
Asynchronous handling:
JavaScript asynchronous operations have evolved over time. The standard way of making an asynchronous call was through the use of callbacks. Recently we’ve gotten better ways to handle these operations, by using Promises, async/await syntax or generators. For that reason, Deno has taken advantage of these modern features.
All async actions in Deno return a promise. This is interesting because the await
keyword is supported on the top level and there is no need to define the function with the async
keyword. This approach allows us to write more readable code when working with asynchronism.
// Await some asynchronous operation
let file = await Deno.open("./my-file.txt");
// Awaiting when starting the server
import { serve } from "https://deno.land/std/http/server.ts";
const server = serve({ port: 3000 });
for await (const req of server) {
req.respond({ body: "Running server!!" });
}
Deno supports promises out of the box. In the first example, the await
keyword waits until Deno.open
is resolved and its result is stored in the file
variable. In the second example, server
is an async iterator and the for await
keywords are used to iterate them, each item will be a new incoming request.
Closing remarks
Deno has incredible features, but there are two points that I think can be improved:
- The permission flags could be handled in a different way like using a file that allows us to set the flags. If your code needs almost all the permissions you will have to use a long command. However, this is not a big issue on itself.
- About the dependencies, NodeJS has a better project organization and the
lock
file is generated automatically. With Deno your dependencies will be placed in one directory, and to lock the dependencies you need to run an additional command. This might not be a good developer experience, but the community is working on some alternatives to change this.
Another point I would like to mention is regarding the examples and tutorials. On the NodeJS side, you can find a lot of examples/tutorials of any library or functionality you may need, while with Deno there are few examples/tutorials. But it will increase in the future for sure.
In this link you will find a small example using Deno and Typescript that shows how to create routes with the oak
module, submit data from a form and generate a CSV file with the provided data.
Conclusion:
Deno is a good alternative for increasing security that allows us to use TypeScript without extra configurations or tooling. It's really nice to write code and see that all the magic occurs behind the scenes. Now there are no more excuses to not use TypeScript.
Having a stable release 1.0, Deno is not yet considered production-ready given that it has some features in the draft phase and needs more time to mature. Right now isn't the time to say goodbye to Node.js, however, in the next couple of years we will see Deno mature and become production ready. I hope that development teams give this tool a chance and that they share their experiences soon.