Table of Content
- A brief overview of Deno
- Exploring other JavaScript runtimes
- Why was Deno created?
- Key advantages of Deno over other JavaScript Runtimes
- Tutorial: Installation & first script
- Tutorial: Initial project structure
- Tutorial: Making HTTP requests and testing in Deno
- Tutorial: Building a REST API with Deno – Dice Roll Example
- Conclusion: Should You Choose Deno for Your Next Project?
A brief overview of Deno
Deno was announced in the JSConf EU Talk in May 2018, and 2 years later, on May 13th, 2020, Deno 1.0 was launched as the first stable version with long-term support. It is a JavaScript runtime based on the V8 JavaScript and WebAssembly engine and written in Rust, focused on delivering essential features “out of the box” like native TypeScript & JavaScript support, Permissions management to increase security, a tool-rich standard library that aims to reduce the dependency on third-party libraries, etc. One fact that most people will find interesting is that both Deno and NodeJS were created by the same person, Ryan Dahl. Dahl abandoned his leadership of the Node.js project in 2012 and, some time later, started the development of Deno to address some of the issues of Node.js described in his talk “10 Things I regret about Node.JS” at the same conference where he presented Deno to the world.
Exploring other Javascript runtimes
There are plenty of JavaScript runtimes out there, but we will focus on the two biggest competitors of Deno, Node.js and Bun.js.
Node.js: The JavaScript runtime by excellence that is also part of the OpenJS Foundation. It was created in 2009 by Ryan Dahl, based on the V8 engine and written mainly in C++. Some of its main traits include:
- NPM as the preferred package manager, standing out as one of the largest Software registries out there (even bigger than PyPi) with hundreds of thousands of 3rd party libraries ready to be integrated into your project as a Node module.
- CommonJS and ES Modules as systems to import packages into the code.
- Huge community support and LTS versions that allow progressive codebase updates to the next LTS version
Bun.js: A baby in the JavaScript runtime world, developed by Oven with Jarred Sumner as the original author, and the 1.0 version was just released on Sep 8th, 2023. It is written in Zig where its API’s are also natively written (instead of JavaScript), and surprisingly, not based on the V8 engine, but in the JavaScriptCore engine from WebKit (used by Safari). Its most prominent features include:
- Built-in SQLite database without relying on external packages.
- One of the fastest runtimes out there, thanks to the Zig and JavaScriptCore combo.
- Native hot reload for real-time updates as the code changes.
- Native TypeScript support.
- Powerful bun command that acts as the package manager, a transpiler, a runner, a bundler, etc.
- Compatible with NPM.
Why was Deno created?
Deno exists mainly because, as previously stated, the creator of Node.js (Ryan Dahl) regretted some decisions he made while creating it and wanted to create a new JavaScript runtime with improved features to address those issues. Some of the reasons (described in the JSConf EU Talk) why he abandoned the Node.js development include:
- Not using promises for async programming since the beginning: Node initially embraced callbacks as the preferred way to deal with asynchronous operations, they work by calling a code block that will be executed once the result for an asynchronous operation is ready (HTTP requests for example), at first glance seems like a good solution to manage async stuff, but…“callback hell” concept sounds familiar to you?, callbacks are nice for single async operations, but once you hit a case where you need the result of an asynchronous operation for another one and so on, you are left with a bunch of nested callbacks that makes the codebase less readable the more layers you add to it. Promises, on the other hand, don’t rely on calling a code block when the async result is ready; initially, they just return a promise object whose results can be extracted within an async function by simply waiting for it to be resolved.
- Poor security focus: By default, a Node program has access to the file system and network operations.
- The infamous node_modules: Imagine that you are creating a Node.js project and want to install Jest to write some unit and integration test, you run npm install to do the job and BOOM you now have 297 additional packages in your node_modules, also want to integrate Jest in your other 10 Node projects?, you get 10 identical copies of the same Jest versions. Would you like to use package A that depends on Lodash 4.17.21 and package B that depends on Lodash 4.17.20? No problem! Here you have both redundant Lodash versions.
Key advantages of Deno over other JavaScript Runtimes
Deno stands out from all the other JavaScript runtimes mainly due to being comparable to a “Swiss Army knife” with all the tools integrated within it. Some of the features that make Deno a good choice for your next project are:
- Secure by default: Deno programs will not have permissions to interact with sensitive functionalities like the network or file system unless explicitly given.
- Native TypeScript support: With Deno, you can, of course, write JS code, but if you prefer TS, you can also do it without additional dependencies.
- Batteries included: Deno comes with essential built-in tools like linter, formatter, test framework, etc.
- Powerful module system: Deno doesn’t come with a package manager by default because it doesn’t need it; it can take ES modules style imports from JSR (preferred registry for Deno) and NPM (fully compatible since Deno 2.0), and also from other places with a URL. Moreover, a global cache is created and shared among all the existing Deno projects, which avoids having multiple copies of the same library for each project.
- Easy installation: Since Deno comes as a single executable binary, a complete installation can be achieved with a single script.
Tutorial: Installation & first script
Now that you have a starter background about Deno, it’s time to have some fun learning how to use it.
Let’s start with the basics. To install Deno on your computer, you need:
Linux or MacOS:
In your shell, run the following command:
curl -fsSL https://deno.land/install.sh | sh
Add the following to your shell configuration file (for example: .bashrc, .zshrc, etc):
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
Windows:
In PowerShell, execute this command:
irm https://deno.land/install.ps1 | iex
Now, let’s test that everything went fine with a classic Hello world. Create a file named main.ts (or whatever name you want) like this:
const name = Deno.args[0] || "You forgot to pass a name”; // You can capture arguments with Deno.args
console.log(`Hi ${name}!`);
Finally, execute your script with (Don’t forget to pass your name as an argument for a warm greeting from your computer):
deno main.ts Toribio # Replace Toribio with your name!
You should have an output like this:
Hi Toribio!
Tutorial: Initial project structure
You can either initialize a Deno project manually, file by file, or you can use the following command to generate a starter project template for you:
deno init your_project_name
This will generate a folder named “your_project_name” (or whatever name you have chosen) with the following files:
- deno.json: Contains the configurations of your project, like tasks and imports, you can check how this file works in the Deno docs.
- main.ts: The main file of your project (it can have the name that you prefer), here you will typically define the “entrypoint” of your project.
- main_test.ts: It contains the test definitions of your project (you can later reorganize your test structure as needed).
The contents of both main.ts and main_test.ts will be overwritten in the next section.
Tutorial: Making HTTP requests and testing in Deno
For this section, we will explore how to use requests and testing in Deno with a simple example. As you can see, this blog lives within the Krasamo webpage, so,let’s make an HTTP request to Krasamo’s homepage and see what we get.
Within the main.ts file, first, we need to perform a GET request to the landing page of Krasamo’s website:
const response = await fetch(“https://krasamo.com");
We can gather the response status code and the HTML body with the following:
const status = response.status;
const strBody = await response.text() // Parsing the body to text is an asynchronous operation that returns a promise, so it requires the await keyword
Finally, let’s organize everything into an asynchronous function that returns the response status code and body as text. Your main.ts should look like this:
// We need to export the function to be able to use it in the tests file
export async function exampleRequest(): Promise<[number, string]> {
const response = await fetch("https://krasamo.com");
const status = response.status;
const strBody = await response.text();
return [status, strBody];
}
const requestData = await exampleRequest();
const requestStatus = requestData[0];
const requestTrimmedBody = requestData[1].substring(0, 15);
console.log(
`Status: ${requestStatus}, First 15 body chars: ${requestTrimmedBody}`,
);
Now that everything is ready, let’s test our request example, and since we are using network resources for the request, we need to add the —allow-net argument to authorize our scripts to perform HTTP requests. The command should look like this:
deno run --allow-net main.ts
If everything went fine, you should see the following output (Only printed the first 15 characters of the body to avoid displaying too much data in your terminal):
Status: 200, First 15 body chars: <!DOCTYPE html>
Nice! You now know how to make an HTTP request from Deno; you can try playing with the fetch function with different websites, you can also extract the response body as a JSON or blob (not just as text), among other stuff that you can deeply explore in the Deno docs.
Testing
Let’s try the native testing capabilities of Deno using the HTTP example that we just made in our main.ts file.
First, erase the contents of your main_test.ts file and import the exampleRequest() function from the main.ts file at the top:
import { exampleRequest } from "./main.ts";
Also, import the assertion functions that we are going to use in the test:
import { assertEquals, assertStringIncludes } from "@std/assert";
One of the criteria that we are going to use to check if the test ran successfully is to check if the services that Krasamo offers match the ones on its landing page. Create an array with all the available services like this:
const krasamoServices = [
"UI/UX Design",
"IoT",
"Mobile Apps",
"Cloud Solutions",
"Digital Transformation",
"Digital Strategy",
];
After that, we need to start the definition of our test:
Deno.test(async function validateResponseTest() {
});
Please note that the Deno.test function receives a function as a parameter; it doesn’t have to be asynchronous, but for this case it is a must since we are making HTTP requests.
Then, within the test definition, retrieve the results from the function made previously in the main.ts file:
const testResponse = await exampleRequest();
const statusCode = testResponse[0];
const textBody = testResponse[1].toLowerCase();
You may notice that the body of the request is being transformed to lowercase because, for this test, string comparisons don’t need to be case-sensitive.
For the next step, let’s define our first assertion that will check if the status code for the request is equal to 200 (OK):
assertEquals(statusCode, 200);
Lastly, we need to create our next series of assertions to check if every service within the krasamoServices array is contained in Krasamo’s landing page. For that, we iterate over all the elements of the array and transform them to lowercase since the comparison needs to be case-insensitive:
krasamoServices.forEach((service) =>
assertStringIncludes(textBody, service.toLowerCase()),
);
Your main_test.ts file should look like this:
import { exampleRequest } from "./main.ts";
import { assertEquals, assertStringIncludes } from "@std/assert";
const krasamoServices = [
"UI/UX Design",
"IoT",
"Mobile Apps",
"Cloud Solutions",
"Digital Transformation",
"Digital Strategy",
];
Deno.test(async function validateResponseTest() {
const testResponse = await exampleRequest();
const statusCode = testResponse[0];
const textBody = testResponse[1].toLowerCase();
assertEquals(statusCode, 200);
krasamoServices.forEach((service) =>
assertStringIncludes(textBody, service.toLowerCase()),
);
The job is done; all that remains is to try our test with the following command (don’t forget the —allow-net parameter for network usage permissions):
deno test --allow-net
If the output looks like this:
------- pre-test output -------
Status: 200, First 15 body chars: <!DOCTYPE html>
----- pre-test output end -----
running 1 test from ./main_test.ts
validateResponseTest ... ok (651ms)
It means that the test ran successfully and that it is true that Krasamo offers all the listed services, so if you need a trustworthy consulting partner, consider getting in touch with us.
You can also dive deeper into how to write tests in Deno with the docs.
Tutorial: Building a REST API with Deno – Dice Roll Example
To close this tutorial section, we are going to create an API for random integer generation with a simulated dice; the constraints for it will be:
- Minimum: 4 faces.
- Maximum: 10000 faces.
- The dice endpoint will be: “/dice/{faces}”, where {faces} will be a path parameter with the number of desired faces.
Let’s begin with the definition of the dice function that will receive the number of faces as a parameter and return a random number between 1 and the number of faces, or -1 if the number of faces is out of range:
function rollDice(faces: number): number {
// Check if the number of faces is in range
// otherwise, return a -1 indicating
// an invalid number of faces
if (faces < 4 || faces > 10000) return -1;
// Generates a random number between
// 1 and faces
const rollResult = Math.floor(Math.random() * faces) + 1;
return rollResult;
}
Then, the URL pattern for our dice API needs to be created to check if the incoming request matches the expected URL path format:
const DiceRoute = new URLPattern({ pathname: "/dice/:faces" });
Now, let’s create the handler function that will capture all the incoming requests to our API
function handler(req: Request): Response {
}
After that, inside the handler function, we need to check if an incoming request matches the DiceRoute expected pattern:
const requestUrl = req.url;
const match = DiceRoute.exec(requestUrl);
If a match was encountered, then we continue with the request validation process; otherwise, we just return a response indicating to the user how to use the API:
if (match) {
// Validation process
}
// Default response
return new Response(
"Hello!, this is the Dice API!, try it with a GET request to /dice/{faces} replacing {faces} with the amount of faces you want to simulate.",
{ status: 418 },
);
So if a match was detected, within the if(match) block, we need to get the {shapes} path parameter and try to transform it into an integer:
const facesStr = match.pathname.groups.faces;
const faces = parseInt(facesStr);
We need to make sure that the faces parameter provided by the user is a number; otherwise, the request will be responded with a status code 400 (Bad request) and with a text stating that the faces parameter needs to be a number:
if (!faces) {
return new Response("The amount of faces must be a number", {
status: 400,
});
}
If the faces parameter is indeed a number, then we try to get a dice roll from the previously defined rollDice function, if we get a -1, it means that the faces number is out of range and a 400 status code (Bad request) should be returned with a text warning the user about the faces range, otherwise, we just return the generated random number:
if (rollResult < 0) {
return new Response(
"The provided faces amount is out of range, it must be between 4 and 10000 inclusive",
{
status: 400,
},
);
}
return new Response(`${rollResult}`);
Finally, at the very end of our main.ts, we serve the handler function:
Deno.serve(handler);
Your main.ts file should look like this:
function rollDice(faces: number): number {
// Check if the number of faces is in range
// otherwise, return a -1 indicating
// an invalid number of faces
if (faces < 4 || faces > 10000) return -1;
// Generates a random number between
// 1 and faces
const rollResult = Math.floor(Math.random() * faces) + 1;
return rollResult;
}
const DiceRoute = new URLPattern({ pathname: "/dice/:faces" });
function handler(req: Request): Response {
const requestUrl = req.url;
const match = DiceRoute.exec(requestUrl);
if (match) {
const facesStr = match.pathname.groups.faces;
const faces = parseInt(facesStr);
if (!faces) {
return new Response("The amount of faces must be a number", {
status: 400,
});
}
const rollResult = rollDice(faces);
if (rollResult < 0) {
return new Response(
"The provided faces amount is out of range, it must be between 4 and 10000 inclusive",
{
status: 400,
},
);
}
return new Response(`${rollResult}`);
}
// Default response
return new Response(
"Hello!, this is the Dice API!, try it with a GET request to /dice/{shapes} replacing {shapes} with the amount of shapes you want to simulate.",
{ status: 418 },
);
}
Run your Dice API with the following command, making sure to include the —allow-net flag since network permissions are needed to serve the API:
deno run —allow-net main.ts
You should get the following output indicating that your Deno API is listening to requests from port 8000:
Listening on http://0.0.0.0:8000/ (http://localhost:8000/)
And that’s all! You can try your API directly in your browser or with a CLI tool like curl. Here is an example with responses:
lloydna@lloydna dice_deno % curl http://localhost:8000/dice/44487
lloydna@lloydna dice_deno % curl http://localhost:8000/dice/444311
lloydna@lloydna dice_deno % curl http://localhost:8000/dice/44441884
lloydna@lloydna dice_deno % curl http://localhost:8000/dice/44444
The provided faces amount is out of range, it must be between 4 and 10000 inclusive
Conclusion: Should You Choose Deno for Your Next Project?
Deno has been a great competitor in the rough world of JavaScript runtimes, offering a secure and efficient development experience with its permission system, native TypeScript support, lots of built-in functionalities, globally cached packages, etc.
In the past, many developers rejected Deno since it didn’t fully support NPM as a package repository, but since version 2.0, this major issue disappeared, and there are a lot of reasons to use Deno or to even transition from another JavaScript runtime to Deno.
Whatever your use case is for a JavaScript runtime, you should consider using Deno as a reliable, easy-to-use, and secure tool.
0 Comments