Learn about Deno modules – the biggest workflow change you’ll encounter if you’re coming from Node.js. Find out how they work and how to best use them, how to make use of Node.js modules and npm packages in Deno, and more.
Node.js is a JavaScript runtime based on Chrome’s V8 engine, developed by Ryan Dahl, and released in 2009.
Deno is a JavaScript runtime based on Chrome’s V8 engine, developed by Ryan Dahl, and released in 2020. It was created with the benefit of a decade’s worth of hindsight. That doesn’t necessarily make it a sequel or superior to Node.js, but it deviates from that path.
See also:
- Our guide to Deno, including our index of Deno content (scroll to the end)
- A comparison of Node.js vs Deno, and guide to picking to the right tool for the situation
The headline differences: Deno natively supports TypeScript, security, testing, and browser APIs. Module handling receives less attention, but it’s possibly the largest change to how you create JavaScript applications. Before discussing Deno, let me take you back to a simpler time…
Key Takeaways
- Deno, a JavaScript runtime based on Chrome’s V8 engine, deviates from Node.js by natively supporting TypeScript, security, testing, and browser APIs. It also opts for ES2015 Modules which are imported from an absolute or relative URL, unlike Node.js which uses CommonJS.
- Deno does not have a package manager like npm in Node.js. Instead, it downloads and caches a module in a global directory the first time its URL is encountered in a script, meaning only a single copy of a specific module version is required no matter how many projects reference it.
- Deno allows for versioning of module URLs so that a particular code release can be referenced. It also supports the use of a single dependencies file to import every module used within a project, or an import map which assigns a name to a full or partial URL.
- The platform has built-in security, limiting file system and network access, and provides an integrity checking option. It also supports the use of Node.js APIs and cross-platform modules which work on both Node.js and Deno without special treatment are likely to become more common as the JavaScript runtime ecosystem evolves.
Node.js Modules
JavaScript didn’t have a standard module system in 2009. This was partly because of its browser heritage and ES6 / ES2015 was several years away.
It would have been inconceivable for Node.js not to provide modules, so it adopted CommonJS from a choice of community workarounds. This led to the development of the Node Package Manager, or npm, which allowed developers to easily search, use, and publish their own JavaScript modules.
npm usage grew exponentially. It’s become the most popular package manager ever devised and, by mid-2020, hosts almost 1.5 million modules with more than 800 new ones published every day (source: modulecounts.com).
Deno Modules
Deno opts for ES2015 Modules which you import
from an absolute or relative URL:
import { something } from 'https://somewhere.com/somehow.js';
The script at that URL must export
functions or other values accordingly, e.g.
export function something() {
console.log('something was executed');
}
Deno uses an identical module system to that implemented in modern web browsers.
Node.js also supports ES2015 modules … but it’s complicated and remains experimental. CommonJS and ES2015 modules look similar, but work in different ways:
- CommonJS loads dependencies from the file system on demand while executing the code.
- ES modules are pre-parsed from URLs in order to resolve further imports before code is executed.
Node.js must continue to support CommonJS as well as handle intermixed ES modules. It therefore presumes:
- files ending
.cjs
use CommonJS - files ending
.mjs
use ES modules - files ending
.js
are CommonJS UNLESS the closestpackage.json
sets"type": "module"
ornode
is executed with an--input-type=module
option.
It’s understandable why Deno opted for the single standard module system. However, npm was central to Node’s success, so it’s surprising to discover Deno does away with it.
There is no package manager.
One criticism of npm is the sheer size of each project’s node_modules
directory. It can reach hundreds of megabytes as modules require specific versions of other modules.
Deno downloads and caches a module in a global directory the first time its URL is encountered in a script. Therefore, only a single copy of a specific module version is required no matter how many projects reference it.
I know you’re thinking: “ahh, but what if…”
… but Deno has options for solving the problems raised by module URLs.
Unreliable URLs
URLs can fail temporarily, change, or disappear forever. This is a problem for any package manager and npm has encountered issues in the past (it also permits installation from a URL).
For mission-critical Node.js applications, it’s advisable to add your node_modules
directory to your project’s Git/other repository.
Deno supports a similar option. You can set the DENO_DIR
environment variable to a directory path within your current project, e.g.
DENO_DIR=~/myproject/deno_modules`
In Windows
cmd
use:
> set DENO_DIR="C:\myproject\deno_modules"
or Windows Powershell:
> $env:DENO_DIR="C:\myproject\deno_modules"
Deno will cache modules to that directory when your application runs so they can be added to the project’s source control repository.
You could also consider bundling your dependencies into a single JavaScript or TypeScript file. The Deno bundle command can do this in a single step:
deno bundle myscript.js myscript.bundle.js
Where myscript.js
is your entry script normally executed with deno run
. The resulting self-contained myscript.bundle.js
file could be deployed to a live server.
Bundling with top-level awaits
Deno supports top-level await
: there’s no need to wrap await
calls in an anonymous async
function. Unfortunately, top-level await fails in bundling so a wrapper function must be added. It’s a known issue and will be fixed in a future release.
Finally: be wary of random Deno modules on unusual URLs! A Deno, Github, or Bitbucket URL with good documentation and community input will generally be safer.
Module Versioning
Ideally, module URLs should be versioned so you’re referencing a particular code release. For example, the Deno standard library allows you to load a specific version of the HTTP server module:
import { serve } from 'https://deno.land/std@0.61.0/http/server.ts';
It’s possible to reference the master branch instead:
import { serve } from 'https://deno.land/std/http/server.ts';
but this would download the latest version and a future release could be incompatible with your application.
It’s possible to release Deno modules on your own server using a similar versioning convention, but your site could receive a lot of traffic as it became popular. A more robust method is to use a repository on a service such as GitHub and assign a git tag to every release. Services such as denopkg.com and unpkg.com can be used to provide a publicly-versioned module URL.
Multiple Module Mentions
You may need to reference the same module URL in many files throughout your application’s codebase. When you want to update that module, the URL would need to be changed in multiple places. A search and replace would work, but it’s clunky, error-prone, and increases the chances of merge conflicts.
Alternatively, you can use a single dependencies file which imports every module you’re using within the project. It is typically named deps.js
or deps.ts
:
// deps.js: module dependencies
// all std path module funtions
export * as path from 'https://deno.land/std@0.61.0/path/mod.ts';
// some std datetime module functions
export { parseDate, currentDayOfYear } from 'https://deno.land/std@0.61.0/datetime/mod.ts';
You can then reference Deno modules from deps.js
in any other project file:
import { path, currentDayOfYear } from './deps.js';
console.log( path.sep );
console.log( currentDayOfYear() );
You need only change a single URL reference in deps.js
when a module is updated.
An alternative option is an import map. This is a small JSON file, typically named import_map.json
, which assigns a name to a full or partial URL:
{
"imports": {
"path/": "https://deno.land/std@0.61.0/path/",
"datetime/": "https://deno.land/std@0.61.0/datetime/"
}
}
You can reference the import map names in any script:
import * as path from 'path/mod.ts';
import { currentDayOfYear } from 'datetime/mod.ts';
console.log( path.sep );
console.log(currentDayOfYear());
The JSON file is then imported when executing the application with deno run
:
deno run \
--importmap=import_map.json \
--unstable \
myscript.js
Import maps are currently an unstable feature so the --unstable
flag is required. The feature may change in future Deno releases.
Investigating Integrity
Code referenced from a URL could be changed or hacked without your knowledge. High-profile sites have been compromised because they linked directly to third-party client-side code. Imagine the damage a script could do if it had access to server resources.
Deno has built-in security so scripts must be executed with flags such as
--allow-read
and--allow-net
to limit file system and network access. This will help prevent some issues, but it’s no substitute for verifying module integrity!
Deno provides an integrity checking option. It’s easiest if you’re using a single dependencies file (as described above):
// deps.js: module dependencies
// all std path module funtions
export * as path from 'https://deno.land/std@0.61.0/path/mod.ts';
// some std datetime module functions
export { parseDate, currentDayOfYear } from 'https://deno.land/std@0.61.0/datetime/mod.ts';
The following deno
command generates a lock.json
file containing a checksum of all imported Deno modules:
deno cache --lock=lock.json --lock-write deps.js
When another developer clones your project, they can reload every module and verify the integrity of each to guarantee they are identical to yours:
deno cache --reload --lock=lock.json deps.js
Integrity checking is not enforced by Deno. It may be best to run these processes as automated Git hooks or similar.
Using Node.js Modules
Many Node.js APIs have been replicated for Deno — see deno.land/std/node. It’s not a complete list, but you’ll find common file, event, buffer, and utility modules.
A collection of almost 800 third-party Deno modules is available at deno.land/x. There are Express.js-like frameworks, database drivers, encryption functions, command-line tools, and more.
You’ll also discover curated lists of popular modules such as Awesome Deno.
However, you may be able to import any of the 1.5 million Node.js modules. Several CDNs can convert npm/CommonJS packages to ES2015 module URLs, including:
- Skypack.dev
- jspm.org
- unpkg.com (add a
?module
querystring to a URL)
Whether the module you need works without problems in Deno is another matter.
Fortunately, cross-platform modules which work on both Node.js and Deno without special treatment are likely to arrive as the JavaScript runtime ecosystem evolves.
More Module Matters
Referencing module URLs is controversial and may be disconcerting for those coming from the hugely popular npm. That said, Deno has simplified JavaScript module usage. It addresses several npm criticisms while alleviating many potential side-effects of ES2015 modules.
But it’s far from perfect.
Publishing npm modules is painless and searching npmjs.com is simple. Your search term may return 500 results, but choice paralysis is minimized by ranking packages by popularity, quality, and maintenance factors.
Submitting code to Deno’s third-party module list is more difficult. Modules must pass automated tests, but there’s no guarantee of quality and search results are ordered alphabetically. The existing system is unlikely to be sustainable once it hits a few thousand modules.
Updating packages is also easy in npm. You can run npm outdated
to view a list of updates or just npm install
when looser version numbers are referenced in package.json
.
There is no equivalent update-checking option in Deno. Package manager-like projects are available including Trex, Update Deno Dependencies, and deno-check-updates but these often depend on import maps and will always rely on semantically-versioned URLs.
Should You Switch to Deno?
Node.js is not dead. It’s mature and has a decade’s worth of modules, techniques, documentation, and experience behind the runtime.
Deno leverages much of that knowledge, but it’s very new and will evolve rapidly over the coming years. It’s possibly too early to bet on Deno for a major app, but there’s less risk for smaller projects. Those already using TypeScript or coming from other languages may enjoy an easier experience, but Node.js developers won’t have any trouble transitioning to Deno and back again.
However, Deno has an interesting benefit:
- its module system is identical to client-side JavaScript
- it’s implementing many browser APIs: you can reference a
window
object, set event listeners, launch Web Workers, make remote server requests with the Fetch() API, and more.
The dream of isomorphic JavaScript libraries which work on either the client or server has taken a significant step forward.
Deno Foundations
Get up to speed with Deno. Our Deno Foundations collection helps you take your first steps into the Deno world and beyond, and we’re adding to it constantly. We’ll bring you the tutorials you need to become a pro. You can always refer to our index as it’s updated at the end of our Introduction to Deno:
FAQs About Deno Modules
Deno modules are units of code in Deno, a secure runtime for JavaScript and TypeScript. Modules in Deno are similar to CommonJS modules in Node.js and ES6 modules in modern JavaScript. They enable developers to organize and share code by breaking it into reusable and encapsulated pieces.
Creating a module in Deno is simple. You can create a new file (e.g., module.ts
), define your functions or classes, and then export them using the export
keyword. Other Deno scripts can then import and use these modules.
To import a module in Deno, you can use the import
keyword followed by the module’s path or URL. Deno supports both local and remote imports.
Yes, Deno supports importing modules directly from URLs, allowing you to use third-party modules hosted on package registries or GitHub repositories.
Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.