DEV Community

Cover image for Welcome to Deno Land.
Lorenzo
Lorenzo

Posted on

Welcome to Deno Land.

Full GitHub source code of the final example.

Less than one month ago, Deno hit his first 1.0 release after 2 years of working on it. If you never heard about it, Deno is an environment like his brother NodeJS. It's secure by default runtime written in Rust (extremely performant, in-memory operations safety and secure multi-paradigm language) with first-class TypeScript support, which means it comes with a built-in TS compiler inside the environment, shipped in one single executable file with a set of a reviewed standard module that shares V8 Engine and the Author Ryan Dahl with his big brother NodeJS.

V8 is the fastest JavaScript engine written in C++ and used by Chrome. SpiderMonkey is the original one used by Mozilla Firefox. The job of an engine is to parse, build an Abstract Syntax Tree and produce a Bytecode and an optimized code by interpreting and compiling our code.

Ok, but why do we need Deno? Ryan Dahl wants to use new technologies and bring to JavaScript developers to be more productive with:

  • Strictly Typed Language without configure it
  • Based on modern features of the JavaScript language
  • Same globals in Frontend and Backend (window)
  • Browser Compatible API through window object: unless using Deno namespace you can run your Deno programs inside Browser
  • Standard JavaScript Module syntax with import/export
  • Standard Library approved by Deno creators (100% trusted)
  • Security by default with a Sandbox: can't do anything outside it
  • Decentralized modules, we don't have to install packages and create a black hole inside our project folder
  • Comes with a set of tools for: testing, formatting, watching, bundling, etc... (more standardization)
  • Promises based while NodeJS is callback based
  • Await at root level
  • Style Guide for more opinionated module creations

So why spend times on Deno? If you already know NodeJS, if you fall in love with TypeScript, don’t want to have millions of bytes on node_modules for every project and you want to use the latest JavaScript features, well Deno might be what you’re looking for. But remember, it's not production-ready!

Is it difficult to learn? Well, it's build with ideas of Node and if you already know JavaScript and TypeScript you have a short learning curve.

However, we have some cons, such as:

  • Not production-ready
  • Not a huge ecosystem
  • NodeJS will be the main choice for the next few years

 Playground and Local Env

Tips: there are some playgrounds online like repl.it, deno.town or Other Playground.

Fist of all we need to install it.
In my MacBook I've installed with Brew:

brew install deno

it will installed in /usr/local/bin folder.
Let's try if our installation is working well:

$ deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕

Yeah 🎉🦕! We got the welcome message from a TypeScript source that's located somewhere in the net 👻
With deno command we can launch a REPL (Read-Eval-Print-Loop) environment. Let's see a very simple example with a fetch
from JSONPlaceholder:

Deno 1.0.5
exit using ctrl+d or close()
> fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => response.json())
  .then(json => console.log(json))
Promise { <pending> }
> { userId: 1, id: 1, title: "delectus aut autem", completed: false }

And, yes! We can use Browser API, while with NodeJS we need to install something like node-fetch to use fetch! Another cool thing is we can use import/export instead of CommonJS for our modules.

First Step

Now we can ready to write our first function in plain JavaScript in a file and try to run in Deno, isn't?

deno run demo.js

We need to use run like npm. At this point we run a process in a sandbox.

// demo.js
import sum from './math.js';
console.log(sum(10, 12));

and:

// math.js
const sum = (a: number, b: number): number => a + b;

export default sum;

and I can run with deno run demo.js.
Rename both file from 'js' to 'ts' and change the extension of the import.
As you can see, Deno wants an explicit extension of the module we are going to import: you will have to be explicit and do import * as foo from "./foo.ts".
This will generate an error on VSCode, and this is the right time to use the plugin to extend the "language service" of our preferred IDE.

Enable Deno on VSCode

To have code completion on namespace Deno, I need to extend VSCode language service.

For Visual Studio Code there is an official extension to have Deno support on it. If you have some trouble with this plugin, check your TypeScript version, maybe you need to force to use a global TypeScript updated version instead of the internal one shipped with VSCode.

Of course you can use your preferred Editor such as JetBrains, Vim and others but you need to check if there is full support for Deno.

A more complex (😅) example

So, let's go with add some feature to our previous example:

// demo.ts
import sum from './math.ts';
if (Deno.args.length >= 2) {
    const numberOne = parseInt(Deno.args[0]);
    const numberTwo = parseInt(Deno.args[1]);
    console.log(`The result is: ${sum(numberOne, numberTwo)}`);
} else {
    console.log(`C'mon give me some number 🦕`);
}

How Deno works

Deno, under the hood, use V8 through a layer called rusty_v8. This layer is the bridge for the JavaScript World and the Rust World. Now the missing part is the Async World, that is done by using a Rust Project called Tokyo Library that allows us to use a Thread Pool and Workers and have the same Event Loop we got with NodeJS. It's quite similar to how NodeJS works, where our JavaScript application communicates with V8, then V8 has a NodeJS API then, instead of Tokyo, we have LibUV for manage Async code, written in C.

Deno comes with own API, written in Rust. Thanks to this API we can access to some cool features like fetch or the window object or have some nice metrics tools and other nice tools.

Basic HTTP

With NodeJS personally I'll use ExpressJS to write a simple HTTPServer and this is the first step we did when starting with NodeJS, so let's do the same step with Deno. In Deno we have a Standard Library where we can find a lot of module and an HTTP module:

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

const server = serve({ port: 8080 });
console.log("http://localhost:8080/");
/* 
    We don't have any callback
    We have for-await without wrapping inside an async function
*/
for await (const req of server) {
console.log(req);
  req.respond({ body: "Hello from Deno Land\n" });
}

Try to launch with deno run index.ts and see what happened:

  • Deno download all the required module (caching dependencies)
  • We have a security error about networking permissions: we need to explicit it

So re-launch with deno run --allow-net index.ts and Ta-da, we have our web server 😬, open your browser and start your coolest navigation at http://localhost:8080.

Please note that permissions-flags need to write before the name of your application!

Caching Dependencies

When we use standard modules or third party modules, we import some TypeScript files from an URL (we can specify the version) and Deno put them in a Caches folder, in MacBook it's located in ~/Library/Caches/deno/deps. When we try to use it again, Deno use the cached one.

It's a best practice to create a deps.ts file where insert all the dependencies of our project and export from it.

Running command

In Unix based OS we have the make command, so, like using npm scripts, we can create a makefile with usefull bash command instead of write all the permission flag every time.

start:
    deno run --allow-net --allow-read --allow-env server.ts

But we have a better way to do it 😎

Nodem...Denon

Before starting with a more complex example, we are going to use Denon the Deno replacement for Nodemon, a wrapper replacement for deno command line when executing our script.

First of all we need to install it:

$ deno install --allow-read --allow-run --allow-write --allow-net -f --unstable https://deno.land/x/denon/denon.ts

Maybe you need to add denon to your path, in my .zshrc I've:

#Denon
export PATH="/Users/<USERNAME>/.deno/bin:$PATH"

Then we need a file similar to 'package.json'. We can use a json file but also, yaml or ts file. To have a json file you can simple type: denon --init (yes, I know, such as npm init), you can check all the "Starter Template File" here:

{
    "$schema": "https://deno.land/x/denon/schema.json",
    "watch": true,
    "allow": [
        "run",
        "env",
        "net"
    ],
    "scripts": {
      "start": "server.ts"
    }
}

Now I can run the command denon start such as npm start 🍾

Expr...Oak

Let's begin our Server application with a Router. We are going to be using Oak as middleware framework to manage HTTP Request/Response like Express, Denv a module similar to DotEnv to manage our environment variables. All dependencies will be exported from deps.ts file:

// deps.ts
export { config } from 'https://deno.land/x/dotenv/mod.ts';
export { 
Application, 
Router, 
RouterContext, 
Status, 
send, 
isHttpError, 
HttpError } from "https://deno.land/x/oak/mod.ts";

Then create the .env file with touch .env (or whatever command/editor you like) and set a PORT:

PORT = 3000

Now we can define an interface as a model for our Todo item:

export default interface Todo {
    id: number;
    title: string;
    completed: boolean;
    userId: number;
}

And now we can write the server.ts application:

import { 
    config, 
    Application,
    Status
} from './deps.ts';
import router from './routes/routes.ts';

// With safe:true config will produce an error if variable is missing.
const { PORT } = config({safe: true});

// Like Express ;)
const app = new Application();

app.addEventListener("error", (evt) => {
    // Will log the thrown error to the console.
    console.log(evt.error);
});


app.use(router.routes());
app.use(router.allowedMethods());

// ctx is the Context Object for handling response/request
app.use((ctx) => {
    ctx.response.status = Status.NotFound;
    ctx.response.type = "json";
    ctx.response.body = {
        message: '404 - Page Not Found'
    }
  });

console.log(`Deno is running on port: ${PORT}`);

await app.listen({ port: parseInt(PORT) })

At this moment we need to create our routes, so in a new folder routes create a new file routes.ts:

import { Router, send } from '../deps.ts';
import { getAllTodos, getTodo } from '../controllers/todos.controller.ts';

const router = new Router();

router.get('/todos', getAllTodos);
router.get('/todos/:id', getTodo);

// This is the static route for static assets
router.get('/', 
    async (context) => {
        await send(context, context.request.url.pathname, {
          root: `${Deno.cwd()}/static`,
          index: "index.html",
        });
    }
)
export default router;

Well, we need to define our controller to export functions handle our Todos items:

import Todo from '../models/Todo.ts';
import { 
RouterContext, 
Status, 
HttpError, 
isHttpError } from '../deps.ts';

/*
We define a very simple function to handle Errors
*/
const requestError = (ctx: RouterContext, err: HttpError | any, message: string = 'Error on request') => {
    if (isHttpError(err)) {
        switch (err.status) {
            case Status.NotFound:
                ctx.response.status = Status.NotFound;
                ctx.response.body = {
                    message
                };
            break;
            case Status.Forbidden:
                ctx.response.status = Status.Forbidden;
                ctx.response.body = {
                    message: "You don't have permissions"
                };
                break;
            default:
                ctx.response.status = Status.InternalServerError;
                ctx.response.body = {
                    message: "Kernel Panic: Internal Server Error x.x !!!"
                };
        }
    } else {
        throw err;
    }
}

export const getAllTodos = async (ctx: RouterContext) => {
    try {
        const res = await fetch('https://jsonplaceholder.typicode.com/todos');
        ctx.response.type = "json";
        if (res.status === 200) {
            const todos: Todo[] = await res.json();
            ctx.response.status = Status.OK;
            ctx.response.body = {
                resultSet: todos
            };
        } else {
            throw ctx.throw(res.status)
        }
    }
    catch(err){
        requestError(ctx, err, 'Error getting all todos');
    }
}

export const getTodo = async (ctx: RouterContext) => {
    try {
        const id = ctx.params && ctx.params.id;
        const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
        ctx.response.type = "json";
        if (res.status === 200) {
            const todo: Todo = await res.json();
            ctx.response.status = Status.OK;
            ctx.response.body = {
                resultSet: todo
            };
        } else {
            throw ctx.throw(res.status)
        }
    } catch(err) {
        requestError(ctx, err, 'Error getting todo');
    }
}

Run with denon start on you terminal window.

Of course this is just a demo and a lot of improvements need to add to the project such as validations and a better class for handling errors. So, play with the full code and improve it and share with the community 😊

You can find the full CRUD application on my GitHub source code of the final example.)

Top comments (0)