Nowadays it is quite natural to take advantage of WebSockets, either with an in-app notification system, facilitating real-time chats, allowing collaborative tools, among many other cases.
In some programming languages or runtime environments, websockets are a real challenge. People often take advantage of community-created solutions or use services to deal with this challenge, but with Deno, it's refreshingly easy.
Introduction
In this article, we are going to create a REST API with two endpoints, one of which will be responsible for issuing an event, which in this case will correspond to a document that contains the message data. And another one that will establish the WebSocket connection that will send messages from the server to the client.
To give you a little more context, in this article we are going to use the following technologies:
Before starting this article, I recommend that you have Deno installed and that you have a brief experience using Node.
Set up project
To get started, navigate to the directory of your choice and run the following command:
deno init .
The above command is expected to have created a set of files in the workspace, with this we have initialized a Deno project and are going to make some changes to the deno.jsonc
file.
Starting by defining some of the commands that we are going to run with task runner deno task
:
{
"tasks": {
"dev": "deno run --watch main.ts"
}
}
Next, let's define some dependencies that need to be imported into the project:
{
// ...
"imports": {
"oak": "https://deno.land/x/oak@v12.5.0/mod.ts",
"evt": "https://deno.land/x/evt@v2.4.22/mod.ts"
}
}
With this last change in deno.jsonc
we can consider the project's configuration as finalized and now it's time to get our hands dirty!
WebSocket Example
EventEmitter's Setup
The next step will be to instantiate the event emitter that will be used in the API, when we use the dependency evt we can create a totally typesafe client and the only thing we need is define a type and pass it as a generic in the .create()
method, like this:
// @/common/emitter.ts
import { Evt } from "evt";
export interface Todo {
title: string;
isDone: boolean;
createdBy: string;
}
export const todoEmitter = Evt.create<Todo>();
Router Definition
Considering the previous point, we can now define each of the routes mentioned in the introduction to this article.
First, we need to import the event emitter that we just created in the previous point. Then we will create the route with the HTTP verb POST
, this route is intended to obtain the message data (which in this case is the Todo). Without forgetting to mention that we need to access the CreatedBy
header, so that later we will have the necessary data to emit an event.
// @/router.ts
import { Router } from "oak";
import { todoEmitter } from "./common/emitter.ts";
export const router = new Router();
router.post("/todos", async (ctx) => {
const createdBy = ctx.request.headers.get("CreatedBy");
if (!createdBy) ctx.throw(400);
const { value } = ctx.request.body({ type: "json" });
const data = await value;
try {
await todoEmitter.postAndWait({ ...data, createdBy });
ctx.response.status = 200;
ctx.response.body = { isSuccessful: true };
} catch {
ctx.throw(400);
}
});
// ...
Now that we've emitted an event whenever a new Todo is created, we can now create the GET
route. First we have to check if the connection can be upgraded, if not we return an error. Next, we'll access the CreatedBy
header to filter the messages that are sent to the client.
In fact, the todoEmitter
is also an Async Iterator, we can iterate directly the instance and then send the WebSocket message if the creator of the Todo is exactly the same as the one in the CreatedBy
header.
// @/router.ts
import { Router } from "oak";
import { todoEmitter } from "./common/emitter.ts";
export const router = new Router();
// ...
router.get("/todos-ws", async (ctx) => {
if (!ctx.isUpgradable) ctx.throw(501);
const createdBy = ctx.request.headers.get("CreatedBy");
if (!createdBy) ctx.throw(400);
const ws = ctx.upgrade();
for await (const todo of todoEmitter) {
if (todo.createdBy === createdBy) {
ws.send(JSON.stringify(todo));
}
}
});
Now we can say that we have each of the routes registered, which allows us to go to the next and last step.
Server Definition
In this step, we will import the routes created in the previous point and make a basic configuration of the API. Like this:
// @/main.ts
import { Application } from "oak";
import { router } from "./router.ts";
const app = new Application({ logErrors: false });
app.use(router.routes());
app.use(router.allowedMethods());
app.listen({ port: 8000 });
To start the process just run this command:
deno task dev
Conclusion
I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.
Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.
Top comments (1)
Nice and clean example. How can I test it?