Learn Deno: Chat app

Installing Deno

There are different ways to install Deno: Using curl, iwr, Homebrew, Chocolatey… See how to install it here. Deno is a single binary executable, it has no external dependencies.

➜  ~ brew install deno
➜ ~ deno --version
deno 1.0.0-rc1
v8 8.2.308
typescript 3.8.3
USAGE:
deno [OPTIONS] [SUBCOMMAND]
OPTIONS:
-h, --help Prints help information
-L, --log-level <log-level> Set log level [possible values: debug, info]
-q, --quiet Suppress diagnostic output
-V, --version Prints version information
SUBCOMMANDS:
bundle Bundle module and dependencies into single file
cache Cache the dependencies
completions Generate shell completions
doc Show documentation for a module
eval Eval script
fmt Format source files
help Prints this message or the help of the given subcommand(s)
info Show info about cache or info related to source file
install Install script as an executable
repl Read Eval Print Loop
run Run a program given a filename or url to the module
test Run tests
types Print runtime TypeScript declarations
upgrade Upgrade deno executable to newest version
ENVIRONMENT VARIABLES:
DENO_DIR Set deno's base directory (defaults to $HOME/.deno)
DENO_INSTALL_ROOT Set deno install's output directory
(defaults to $HOME/.deno/bin)
NO_COLOR Set to disable color
HTTP_PROXY Proxy address for HTTP requests
(module downloads, fetch)
HTTPS_PROXY Same but for HTTPS

Simple “Hello World”

For a simple “Hello world” in Deno, we just need to create a file .js or .ts, and execute it with deno run [file].

// example.ts file
console.log('Hello from Deno 🖐')
➜  deno run example.ts
Compile file:///Users/aralroca/example.ts
Hello from Deno 🖐

Serve an index.html

Deno has his own standard library https://deno.land/std/ so to use their modules we can import it directly from the URL. One of its goals is shipping only a single executable with minimal linkage. This way it’s only necessary to import the URL to their projects, or execute directly with deno run https://... in case of CLIs.

<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<title>Example using Deno</title>
</head>
<body>index.html served correctly</body>
</html>
import { listenAndServe } from "https://deno.land/std/http/server.ts";listenAndServe({ port: 3000 }, async (req) => {
if (req.method === "GET" && req.url === "/") {
req.respond({
status: 200,
headers: new Headers({
"content-type": "text/html",
}),
body: await Deno.open("./index.html"),
});
}
});
console.log("Server running on localhost:3000");
  1. It downloads all the dependencies from http module. Instead of using yarn or npm install, it should install all the needed dependencies before running the project. This happens only the first time, since it's cached. To clean the cache you can use the --reload command.
  2. It throws an error Uncaught PermissionDenied: network access to "127.0.0.1:3000", run again with the --allow-net flag. Deno is secure by default. This means that we can't access to the net or read a file (index.html). This is one of the big improvements over Node. In Node any CLI library could do many things without our consent. With Deno it's possible, for example, to allow reading access only in one folder: deno --allow-read=/etc. To see all permission flags, run deno run -h.
➜ deno run --allow-net --allow-read server.ts
Compile file:///Users/aralroca/server.ts
Server running on localhost:3000

Using WebSockets

WebSockets, UUID, and other essentials in Node are not part of the core. This means that we need to use third-party libraries to use it. Yet, you can use WebSockets and UUID among many others by using Deno standard library. In other words, you don’t need to worry about maintenance, because now it will be always maintained.

import {
WebSocket,
isWebSocketCloseEvent,
} from "https://deno.land/std/ws/mod.ts";
import { v4 } from "https://deno.land/std/uuid/mod.ts";
const users = new Map<string, WebSocket>();function broadcast(message: string, senderId?: string): void {
if(!message) return
for (const user of users.values()) {
user.send(senderId ? `[${senderId}]: ${message}` : message);
}
}
export async function chat(ws: WebSocket): Promise<void> {
const userId = v4.generate();
// Register user connection
users.set(userId, ws);
broadcast(`> User with the id ${userId} is connected`);
// Wait for new messages
for await (const event of ws) {
const message = typeof event === 'string' ? event : ''
broadcast(message, userId); // Unregister user conection
if (!message && isWebSocketCloseEvent(event)) {
users.delete(userId);
broadcast(`> User with the id ${userId} is disconnected`);
break;
}
}
}
import { listenAndServe } from "https://deno.land/std/http/server.ts";
import { acceptWebSocket, acceptable } from "https://deno.land/std/ws/mod.ts";
import { chat } from "./chat.ts";
listenAndServe({ port: 3000 }, async (req) => {
if (req.method === "GET" && req.url === "/") {
req.respond({
status: 200,
headers: new Headers({
"content-type": "text/html",
}),
body: await Deno.open("./index.html"),
});
}
// WebSockets Chat
if (req.method === "GET" && req.url === "/ws") {
if (acceptable(req)) {
acceptWebSocket({
conn: req.conn,
bufReader: req.r,
bufWriter: req.w,
headers: req.headers,
}).then(chat);
}
}
});
console.log("Server running on localhost:3000");
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Chat using Deno</title>
</head>
<body>
<div id="app" />
<script type="module">
import { html, render, useEffect, useState } from 'https://unpkg.com/htm/preact/standalone.module.js'
let ws function Chat() {
// Messages
const [messages, setMessages] = useState([])
const onReceiveMessage = ({ data }) => setMessages(m => ([...m, data]))
const onSendMessage = e => {
const msg = e.target[0].value
e.preventDefault()
ws.send(msg)
e.target[0].value = ''
}
// Websocket connection + events
useEffect(() => {
if (ws) ws.close()
ws = new WebSocket(`ws://${window.location.host}/ws`)
ws.addEventListener("message", onReceiveMessage)
return () => {
ws.removeEventListener("message", onReceiveMessage)
}
}, [])
return html`
${messages.map(message => html`
<div>${message}</div>
`)}
<form onSubmit=${onSendMessage}>
<input type="text" />
<button>Send</button>
</form>
`
}
render(html`<${Chat} />`, document.getElementById('app'))
</script>
</body>
</html>

Third-party and deps.ts convention

We can use third-party libraries in the same way we use the Deno Standard Library, by importing directly the URL of the module.

import { camelCase } from 'https://cdn.pika.dev/camel-case@^4.1.1';
// ...before code
const message = camelCase(typeof event === 'string' ? event : '')
// ... before code
// deps.ts file
export { camelCase } from 'https://cdn.pika.dev/camel-case@^4.1.1';
// chat.ts file
import { camelCase } from './deps.ts';
// ...
const message = camelCase(typeof event === 'string' ? event : '')
// ...

Testing

We are going to build a useless camilize.ts utility to return the text in camelCase with a nice extra, it includes one 🐪 per uppercase letter. Why? To see how to test it with Deno.

/**
* Return the text in camelCase + how many 🐪
*
* @example "this is an example" -> "thisIsAnExample 🐪🐪🐪"
* @param text
* @returns {string}
*/
export function camelize(text: string) {
// @todo
}
➜  deno doc camelize.ts 
function camelize(text: string)
Return the text in camelCase + how many 🐪
import { assertStrictEq } from "https://deno.land/std/testing/asserts.ts";
import { camelize } from "./camelize.ts";
Deno.test("camelize works", async () => {
assertStrictEq(camelize("this is an example"), "thisIsAnExample 🐪🐪🐪");
});
➜  deno deno test
Compile file:///Users/aralroca/test.ts
running 1 tests
test camelize works ... FAILED (0ms)
failures:camelize works
AssertionError: actual: undefined expected: thisIsAnExample 🐪🐪🐪
at assertStrictEq (asserts.ts:224:11)
at test.ts:5:3
at asyncOpSanitizer ($deno$/testing.ts:36:11)
at Object.resourceSanitizer [as fn] ($deno$/testing.ts:70:11)
at TestApi.[Symbol.asyncIterator] ($deno$/testing.ts:264:22)
at TestApi.next (<anonymous>)
at Object.runTests ($deno$/testing.ts:346:20)
failures: camelize workstest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (0ms)
import { camelCase } from "./deps.ts";/**
* Return the text in camelCase + how many 🐪
*
* @example "this is an example" -> "thisIsAnExample 🐪🐪🐪"
* @param text
* @returns {string}
*/
export function camelize(text: string) {
const camelCaseText = camelCase(text);
const matches = camelCaseText.match(/[A-Z]/g) || [];
const camels = Array.from({ length: matches.length })
.map(() => "🐪")
.join("");
return `${camelCaseText} ${camels}`;
}
➜  deno test
Compile file:///Users/aralroca/camelize.ts
running 1 tests
test camelize works ... ok (3ms)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (3ms)

Debugging

In order to debug with Deno:

  1. Add somewhere in your code a debugger; line of code.
  2. Run with --inspect-brk flag. deno run --inspect-brk ... or deno test --inspect-brk ... to debug tests.
  3. Open chrome://inspect page on Chrome.
  4. On the Remote Target section press to “inspect”.
  5. Press the Resume script execution button, the code will pause just in your breakpoint.

Conclusion

We learned about how Deno works by creating a simple chat app in TypeScript. We did it without npm, package.json, node_modules, webpack, babel, jest, prettier… because we don’t need them, Deno simplifies this.

Code of this article

I uploaded the code on my GitHub:

References

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store