loading...
Cover image for Forget NodeJS! Build native TypeScript applications with Deno

Forget NodeJS! Build native TypeScript applications with Deno

deepu105 profile image Deepu K Sasidharan Updated on 11 min read

Originally published at deepu.tech.

Please follow me on Twitter for updates and let me know what can be improved in the post.


Have you heard of Deno? If not you should check it out. Deno is a modern JavaScript/TypeScript runtime & scripting environment. Deno is what NodeJS should have been according to Ryan Dahl who created NodeJS. Deno was also created by Ryan Dahl in 2018 and is built with V8, Rust and Tokio with a focus on security, performance, and ease of use. Deno takes many inspirations from Go and Rust.

In this post let us see what Deno offers and how it compares to NodeJS. You can also watch the same in a talk format I did for Devoxx Ukraine below

Let us install Deno before we proceed.

Install Deno

There are multiple ways to install Deno. If you are on Mac or Linux, you can install it via Homebrew. On Windows, you can use Chocolatey.

# Mac/Linux
brew install deno

# windows
choco install deno

Check the official doc for other installation methods

Please note that Deno is still under active development and hence may not be ready for production use

Now that we have Deno installed, let us look at its features.

Features

  • TypeScript supported out of the box without any transpiling setup
  • Can execute remote scripts
  • Secure by default. No file, network, or environment access by default unless explicitly enabled
  • Provides curated standard modules
  • Supports only ES modules. Modules are cached globally and are immutable
  • Built-in tooling (format, lint, test, bundle and so on)
  • Deno applications can be browser compatible
  • Promise based API(async/await supported) and no callback hell
  • Top-level await support
  • Sub-process using web workers
  • WebAssembly support
  • Lightweight multi-platform executable(~10MB)

Deno does not use NPM for dependency management and hence there is no node_modules hell to deal with, which IMO is a huge selling point

shut up and take my money

TypeScript support

Deno has native support for TypeScript and JavaScript. You can write Deno applications directly in TypeScript and Deno can execute them without any transpiling step from your side. Let us try it

function hello(person: string) {
    return "Hello, " + person;
}

console.log(hello("John"));

Save this to hello.ts file and execute deno hello.ts. You will see Deno compiles the file and executes it.

Deno supports the latest version of TypeScript and keeps the support up to date.

Remote script execution

With Deno, you can run a local or remote script quite easily. Just point to the file or HTTP URL of the script and Deno will download and execute it

deno https://deno.land/std/examples/welcome.ts

This means you can just point to a raw GitHub URL to execute a script, no hassle of installing something. The default security model Deno is applied to remote scripts as well.

Secure by default

By default, a script run with Deno cannot access the file system, network, sub-process, or environment. This creates a sandbox for the script and the user has to explicitly provide permissions. This puts control in the hands of the end-user.

  • Granular permissions
  • Permissions can be revoked
  • Permissions whitelist support

The permissions can be provided via command-line flags during execution or programmatically when using sub-processes.

The available flags are:

--allow-all | -A
--allow-env
--allow-hrtime
--allow-read=<whitelist>
--allow-write=<whitelist>
--allow-net=<whitelist>
--allow-plugin
--allow-run

Please note that flags must be passed before the filename like deno -A file.ts or deno run -A file.ts. Anything passed after the filename will be considered as program arguments.

Let us see an example that creates a local HTTP server:

console.info("Hello there!");

import { serve } from "https://deno.land/std/http/server.ts";

const server = serve(":8000");

console.info("Server created!");

The snippet tries to use the network and hence when you run the program with Deno it will fail with an error

To avoid the error we need to pass the --allow-net or --allow-all flag when running the program. You can also grant access to specific ports and domains as well using a whitelist. For example deno --allow-net=:8000 security.ts

deno security no prompt

Standard modules

Deno provides standard modules like NodeJS, Go or Rust. The list is growing as newer versions are released. Currently available modules are:

  • archive - TAR archive handling
  • colors - ANSI colors on console
  • datetime - Datetime parse utilities
  • encoding - Encode/Decode CSV, YAML, HEX, Base32 & TOML
  • flags - CLI argument parser
  • fs - Filesystem API
  • http - HTTP server framework
  • log - Logging framework
  • media_types - Resolve media types
  • prettier - Prettier formatting API
  • strings - String utils
  • testing - Testing utils
  • uuid - UUID support
  • ws - Websocket client/server

The standard modules are available under https://deno.land/std namespace and are tagged in accordance with Deno releases.

import { green } from "https://deno.land/std/fmt/colors.ts";

ES Modules

Deno supports only ES Modules using a remote or local URL. This keeps dependency management simple and light. Unlike NodeJS, Deno doesn't try to be too smart here, which means:

  • require() is not supported, so no confusion with import syntax
  • No "magical" module resolution
  • Third-party modules are imported by URL(Local and remote)
  • Remote code is fetched only once and cached globally for later use
  • Remote code is considered immutable and never updated unless --reload flag is used
  • Dynamic imports are supported
  • Supports import maps
  • Third-party modules are available in https://deno.land/x/
  • NPM modules can be used, if required, as simple local file URL or from jspm.io or pika.dev

Hence we can any import any library that is available from a URL. Let's build on our HTTP server example

import { serve } from "https://deno.land/std/http/server.ts";
import { green } from "https://raw.githubusercontent.com/denoland/deno/master/std/fmt/colors.ts";
import capitalize from "https://unpkg.com/lodash-es@4.17.15/capitalize.js";

const server = serve(":8000");

console.info(green(capitalize("server created!")));

const body = new TextEncoder().encode("Hello there\n");

(async () => {
    console.log(green("Listening on http://localhost:8000/"));
    for await (const req of server) {
        req.respond({ body });
    }
})();

The import paths can be made nicer by using an import map below

{
    "imports": {
        "http/": "https://deno.land/std/http/",
        "fmt/": "https://raw.githubusercontent.com/denoland/deno/master/std/fmt/",
        "lodash/": "https://unpkg.com/lodash-es@4.17.15/"
    }
}

Now we can simplify the paths as below

import { serve } from "http/server.ts";
import { green } from "fmt/colors.ts";
import capitalize from "lodash/capitalize.js";

const server = serve(":8000");

console.info(green(capitalize("server created!")));

const body = new TextEncoder().encode("Hello there\n");

(async () => {
    console.log(green("Listening on http://localhost:8000/"));
    for await (const req of server) {
        req.respond({ body });
    }
})();

Run this with the --importmap flag deno --allow-net=:8000 --importmap import-map.json server.ts. Please note that the flags should be before the filename. Now you can access http://localhost:8000 to verify this.

Built-in tooling

Deno takes inspiration from Rust and Golang to provide built-in tooling, this IMO is great as it helps you get started without having to worry about setting up testing, linting and bundling frameworks. The below are tools currently available/planned

  • Dependency inspector (deno info): Provides information about cache and source files
  • Bundler (deno bundle): Bundle module and dependencies into a single JavaScript file
  • Installer (deno install): Install a Deno module globally, the equivalent of npm install
  • Test runner (deno test): Run tests using the Deno built-in test framework
  • Type info (deno types): Get the Deno TypeScript API reference
  • Code formatter (deno fmt): Format source code using Prettier
  • Linter (planned) (deno lint): Linting support for source code
  • Debugger (planned) (--debug): Debug support for Chrome Dev tools

For example, with Deno, you can write test cases easily using provided utilities

Let's say we have factorial.ts

export function factorial(n: number): number {
    return n == 0 ? 1 : n * factorial(n - 1);
}

We can write a test for this as below

import { test } from "https://deno.land/std/testing/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { factorial } from "./factorial.ts";

test(function testFactorial(): void {
    assertEquals(factorial(5), 120);
});

test(function t2(): void {
    assertEquals("world", "worlds");
});

Browser compatibility

Deno programs or modules can be run on a browser as well if they satisfy the below conditions

  • The program must are written completely in JavaScript and should not use the global Deno APIs
  • If the program is written in Typescript, it must be bundled as JavaScript using deno bundle and should not use the global Deno APIs

For browser compatibility Deno also supports window.load and window.unload events. load and unload events can be used with window.addEventListener as well.

Let us see below sample, this can be run using deno run or we can package it and execute in a browser

import capitalize from "https://unpkg.com/lodash-es@4.17.15/capitalize.js";

export function main() {
    console.log(capitalize("hello from the web browser"));
}

window.onload = () => {
    console.info(capitalize("module loaded!"));
};

We can package this using deno bundle example.ts browser_compatibility.js and use the browser_compatibility.js in an HTML file and load it in a browser. Try it out and look at the browser console.

Promise API

Another great thing about Deno is that all of its API is Promise based which means, unlike NodeJS we do not have to deal with callback hells. Also, the API is quite consistent across standard modules. Let us see an example:

const filePromise: Promise<Deno.File> = Deno.open("dummyFile.txt");

filePromise.then((file: Deno.File) => {
    Deno.copy(Deno.stdout, file).then(() => {
        file.close();
    });
});

But we said no callbacks right, the good thing about Promise API is that we can use async/await syntax, so with that, we can rewrite above

const filePromise: Promise<Deno.File> = Deno.open("dummyFile.txt");

filePromise.then(async (file: Deno.File) => {
    await Deno.copy(Deno.stdout, file);
    file.close();
});

Run deno -A example.ts to see it in action, don't forget to create the dummyFile.txt with some content

Top-level await

The above code still uses a callback, what if we can use await for that as well, luckily Deno has support for the top-level await proposal(Not supported by TypeScript yet). With this, we can rewrite the above

const fileName = Deno.args[0];

const file: Deno.File = await Deno.open(fileName);

await Deno.copy(Deno.stdout, file);

file.close();

Isn't that neat? Run it as deno -A example.ts dummyFile.txt

Subprocess using web workers

Since Deno uses the V8 engine which is single-threaded, we have to use a sub-process like in NodeJS to spawn new threads(V8 instance). This is done using service workers in Deno. Here is an example, we are importing the code we used in the top-level await example in the subprocess here.

const p = Deno.run({
    args: ["deno", "run", "--allow-read", "top_level_await.ts", "dummyFile.txt"],
    stdout: "piped",
    stderr: "piped"
});

const { code } = await p.status();

if (code === 0) {
    const rawOutput = await p.output();
    await Deno.stdout.write(rawOutput);
} else {
    const rawError = await p.stderrOutput();
    const errorString = new TextDecoder().decode(rawError);
    console.log(errorString);
}

Deno.exit(code);

You can run any CMD/Unix command as a subprocess like in NodeJS

WebAssembly support

WebAssembly is one of the most innovative features to have landed on the JavaScript world. It lets us use programs written in any compatible language to be executed in a JS Engine. Deno has native support for WebAssembly. Let us see an example.

First, we need a WebAssembly(WASM) binary. Since we are focusing on Deno here, let's use a simple C program. You can also use Rust, Go or any other supported language. In the end, you just need to provide a compiled .wasm binary file.

int factorial(int n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

We can convert this to WASM binary using the online converter here and import it in our TypeScript program below

const mod = new WebAssembly.Module(await Deno.readFile("fact_c.wasm"));
const {
    exports: { factorial }
} = new WebAssembly.Instance(mod);

console.log(factorial(10));

Run deno -A example.ts and see the output from the C program.


A Deno application in action

Now that we have an overview of Deno features, let's build a Deno CLI app

Run deno --help and deno run --help to see all options that can be passed when you run a program. You can learn more about Deno features and API in the Deno website and manual

Let's build a simple proxy server that can be installed as a CLI tool. This is a really simple proxy, but you can add more features to make it smarter if you like

console.info("Proxy server starting!");

import { serve } from "https://deno.land/std/http/server.ts";
import { green, yellow } from "https://deno.land/std/fmt/colors.ts";

const server = serve(":8000");

const url = Deno.args[0] || "https://deepu.tech";

console.info(green("proxy server created!"));

(async () => {
    console.log(green(`Proxy listening on http://localhost:8000/ for ${url}`));

    for await (const req of server) {
        let reqUrl = req.url.startsWith("http") ? req.url : `${url}${req.url}`;

        console.log(yellow(`URL requested: ${reqUrl}`));

        const res = await fetch(reqUrl);
        req.respond(res);
    }
})();

Run deno --allow-net deno_app.ts https://google.com and visit http://localhost:8000/. You can now see all the traffic on your console. You can use any URL you like instead of Google.

Lets package and install the app.

deno install --allow-net my-proxy deno_app.ts

If you want to override the file use deno install -f --allow-net my-proxy deno_app.ts. You can also publish the script to an HTTP URL and install it from there.

Now just run my-proxy https://google.com and viola we have our own proxy app. Isn't that simple and neat.


Conclusion

Let us see how Deno compares against NodeJS and why I believe it has great potential

Why is Deno better than NodeJS

I consider Deno to be better than NodeJS for the following reasons. The creator of NodeJS thinks the same I guess

  • Easy to install - Single lightweight binary, built-in dependency management
  • Secure by default - Sandboxed, Fine-grained privileges and user-controlled
  • Simple ES module resolution - No smart(confusing) module system like NodeJS
  • Decentralized and globally cached third-party modules - No node_modules hell, efficient
  • No dependency on package managers or package registries(No NPM, No Yarn, No node_modules)
  • Native TypeScript support
  • Follows web standards and modern language features
  • Browser compatibility - Ability to reuse modules in browser and Deno apps
  • Remote script runner - Neat installation of scripts and tools
  • Built-in tooling - No hassle of setting up tooling, bundlers and so on

Why does it matter

Why does it matter, why do we need another scripting environment? Isn't JavaScript ecosystem already bloated enough

  • NodeJS ecosystem has become too heavy and bloated and we need something to break the monopoly and force constructive improvements
  • Dynamic languages are still important especially in the below domains
    • Data science
    • Scripting
    • Tooling
    • CLI
  • Many Python/NodeJS/Bash use cases can be replaced with TypeScript using Deno
    • TypeScript provides better developer experience
    • Consistent and documentable API
    • Easier to build and distribute
    • Does not download the internet all the time
    • More secure

Challenges

This is not without challenges, for Deno to succeed it still has to overcome these issues

  • Fragmentation of libraries and modules
  • Not compatible with many of the NPM modules already out there
  • Library authors would have to publish a Deno compatible build(Not difficult but en extra step)
  • Migrating existing NodeJS apps will not be easy due to incompatible API
  • Bundles are not optimized so might need tooling or improvements there
  • Stability, since Deno is quite new (NodeJS is battle-tested)
  • Not production-ready

If you like this article, please leave a like or a comment.

You can follow me on Twitter and LinkedIn.

Cover image credit: Random image from the internet

Posted on by:

deepu105 profile

Deepu K Sasidharan

@deepu105

JHipster co-lead, Polyglot dev, Cloud Native Advocate, Developer Advocate @Adyen, Author, Speaker, Software craftsman. Loves simple & beautiful code. bit.ly/JHIPSTER-BOOKS

Discussion

pic
Editor guide
 

Personally I use TypeScript and I absolutely love the dev experience, however I think that we're betting on the wrong horse; At the end of the day the underlying tech (JS) is the most important and not the superset. What guarantees that ts won't be another coffeescript?

 

Unlike Coffescript, TS doesn't try to move sway from JS syntax and is backed by MS and Google who uses it extensively so its not gonna go away like CS. Also JS is taking inspiration from TS for features and since TS is syntax superset it would be easier to migrate to JS if required. Personally I don't think TS is going away any time soon.

 

There are no guarantees in life my friend, but I do know that coffeescript is so different from typescript. If you compare the syntax its much harder to start on coffeescript than typescript. I remembered when I was trying to learn both it took time to grasp coffeescript than typescript because of the way the syntax is implemented, while on typescript it was easy an hour of learning will get you going. I say this because I had the first hand experience on my onboarding at work we had to maintain a coffescript codebase it was a bit difficult.

 

Been waiting for deno they said production on version 1 will be ready last january. hehe. just excited for this. anyway I have question about a npm package that is built purely in typescript. Can deno still used it as a library? thanks.

 

Well its Open source and as an open source maintainer I know first hand how schedules go. People are doing it on their free time so nothing is guaranteed.

It doesn't matter if a package is built in TS or JS, what matters is how the module is bundled. If its bundled as an ES module then it should work like the Lodash package I showed in sample. Also Deno is building a node compatibility module to support non ES npm modules

 

ohh. Its easy to migrate then by just changing tscofig. Thank you sir.

Anyway I watch the talk given by ryan dahl he said that "no, nodejs wont be compatible with deno" if he changed his mind this would be awesome. I can still use express.

I don't think its as easy as changing minds. NodeJs and Deno are built upon different architecture. The core module would be different and thus incompatible. Also, deno is supposed to use a different event-loop, with different kinds of stuff. Compatibility should be an issue here.

I don't think there is an issue with event loops, underneath its still V8 engine. The real issue is compatibility with existing nodeJS modules

Yup compatibility with node modules would be the issue. I was talking about event loops because the methods dependent on event loop in node might not be same as deno (of which I am not sure of) like process.nextTick or setImmediate. In that case compatibility might be issue. Basically if any underlying core packages change their API or its working its an compatibility issue.

 

After reading this, I feel I should definitely give it a try.

 

Try it, the experience is much nicer than Node

 

Interesting to see how this pans out. I don't have enough experience with Node to form an opinion about this, but it's always nice to see different platforms moving the industry forward. Even if it doesn't end up being a success, it would definitely be great to see Node adopt the good parts of this.

That's what I like about TypeScript. Some of the really good things are being adopted by TC39 to go into the ES spec.

 

You are absolutely right

 

Thanks for sharing, well written. But I think that Deno is somthing to watch and try for now, but I won't bet on it, I would love to see it grow but it's risky, the sad part for me is "Migrating existing NodeJS apps will not be easy due to incompatible API".

 

For info, part of the Deno std library is a node polyfill which will allow npm modules to be run as part of deno. It's still in its infancy, but there is already support for some of the more common API functions in node. See more here: github.com/denoland/deno/tree/mast...

 

Yes and I hope it helps in Deno adoption

 

I have a question on the url based module loading. If I would require to build a pure offline application how would I load the modules? Are they loaded at dev time and then bundled into a large js file? Would I have to store a local copy, which is then bundled? Or something totally different?

 

Deno team recommends creative a lib.ts file which imports all needed libs so that you can bundle it with the app and use it offline as well. See this deno.land/std/manual.md#linking-to...

 

Anything on editors/ide support ?

 

All typescript supported IDEs should work perfectly fine like VSCode, IntelliJ

 

I mean, when you do import from URLs (remote or local) does auto-completion works?

I don't think that works

For now yes, but they should come up with a way of loading definition files from the remote source especially in the VSCode plugin

 

Excellent article! Thanks for sharing! :D

 

You are welcome and thank you

 

Very good article, thank you!! :)

 

You are welcome and thank you

 

deno != node

 

Indeed and I think the naming was intentional

 

Founder of Node == founder of Deno

 

aaaand... Typescript team just shipped 3.8 with top level await

 
 

I hope Deno will succeed, thanks for the article.

 

Sweet! I've been waiting for this.