Deno (Javascript/Typescript Environment)

Deno es un entorno para correr Typescript o Javascript en el lado del servidor y se construyó con Rust, permitiendo interactuar con el sistema operativo (Windows, macOS, Linux u otro). Deno incorpora una API de navegador por lo que es trasnparente usar sentencias como fetch y eventos, de este modo se logra unificación (isomorfismo) y ampliación para contemplar, por ejemplo, acceso y manipulación de archivos y bases de datos, tu propio servidor web (http), micro-servicios web (API-Rest), una interfaz de línea de comandos, o simplemente scripts en el servidor.

Requiere conocimientos en Javascript, Typescript y lo esencial del navegador

Instalación de Deno

En Windows debes abrir una terminal de PowerShell y ejecutar lo siguiente:

irm https://deno.land/install.ps1 | iex

Si tienes instalado Chocolatey (un gestor de paquetes de Windows), podría usarse el comando: choco install deno

Para instalar Deno en macOS o Linux ejecutas desde la línea de comandos la siguente sentencia:

curl -fsSL https://deno.land/install.sh | sh

Y cuando requieras actualizar el paquete simplemente ejecutas:

deno upgrade

Para Docker se puede usar la imágen denoland/deno:latest. Puedes probar ejecutando:

docker pull denoland/deno:latest
docker run -it denoland/deno sh

Ejemplo esencial

const hello = (name: String): String => {
    return "Hi " + name + "!";
}

console.log(hello("there"));

Abres un editor y guardas el archivo, por ejemplo test.ts, luego ejecutas desde la terminal:

deno run test.ts

Servidor Web con Deno

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

const port = 8000;
const web = serve({ port: port });
const home = (req: any) => req.respond({ body: "Hi there!" });

console.log("http://localhost:" + port);

for await (const req of web) {
    home(req);
}

Esta vez guardas el archivo, por ejemplo server.ts, y luego ejecutas desde la terminal:

deno run --allow-net server.ts

En este caso debemos agregar el indicador --allow-net para habilitar el uso de un recurso de red

Para compilar en binario se ejecuta lo siguiente:

deno compile --allow-net server.ts

Si interactuas con archivos se debe agregar al menos el indicador --allow-read (o --allow-write si se escribe en disco)

Servidor Web con Oak

Oak es un “web framework” liberado por el equipo de Deno. Veamos un ejemplo sencillo para ilustrar una implementación inicial:

import { Application } from "https://deno.land/x/oak/mod.ts";

const port = 8000;
const uri = `127.0.0.1:${port}`
const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hi there!";
});

console.log(uri);
await app.listen(uri);

Servidor Web con Express & Deno

Actualmente es posible usar Expess desde Deno. Para ello iniciamos un proyecto de código en una carpeta nueva, por ejemplo, ejecutando desde la terminal lo siguiente:

mkdir project
cd project
deno init

project se refiere al nombre asignado para el nuevo proyecto de código (de modo que se susituye por un nombre deseado).
deno init nos crea unos archivos iniciales de configuración con un archivo de código main.ts (incluso uno para test)

En el archivo main.ts dejaremos un código como el siguiente:

import express, { Request, Response } from "npm:express@5";

const port: number = Number(Deno.env.get("RUN_PORT")) || 3000;
const app = express();

app.get("/", (req: Request, res: Response) => {
  return res.send("Hi there!");
});

app.get("/data", (req: Request, res: Response) => {
  return res.json({ ping: "pong" });
});

app.listen(port, () => {
  console.log(`Server listening in ${port}`);
});

Para lanzar el nuestro servico ejecutamos:

deno run --allow-net --allow-read --allow-env --allow-sys main.ts

Podríamos usar --allow-all pero así indicamos las banderas necesarias.
--allow-sys en principio no se requiere pero al conectar una base de datos como MongoDB se necesitará.

Ejemplo de Deno con MongoDB

Podemos crear una instancia de MongoDB con sus volumenes de almacenamiento respectivo usando contenedores (Docker o Podman). Por ejemplo, preparamos un entorno de MongoDB con los siguientes comandos:

docker volume create mongodb
docker volume create mongocfg
docker run --name mongodb \
-p 27027:27017 \
-v mongodb:/data/db -v mongocfg:/data/configdb \
-e MONGODB_INITDB_ROOT_USERNAME=root -e MONGODB_INITDB_ROOT_PASSWORD=password \
-d mongo:8.0
docker exec -it mongodb bash
mongosh

Con lo anterior se habilita el puerto 27027 y los volumenes para datos y configuracion de MongoDB, además se indican un par de variables de entorno (para las credenciales).
El comando mongosh nos abre la consola de MongoDB

Dentro de la base de datos podemos inicializar una colección con algún dato. Para ello ejecutamos, una por una, las siguientes sentencias (dentro de mongosh):

use demo

db.messages.insertOne({
  name: "John",
  message: "Hi"
});

show collections

db.messages.find()

exit

A continuación tomaremos como base el ejercicio anterior y al implementar el acceso a los datos desde MongoDB, tendremos un código como el siguiente:

import express, { Request, Response } from "npm:express@5";
import { MongoClient } from "npm:mongodb";

const dburi: string = Deno.env.get("DB_URI");
const dbhost: string = Deno.env.get("DB_HOST") || "localhost";
const dbport: number = Number(Deno.env.get("DB_PORT")) || 27017;
const dbname: string = Deno.env.get("DB_NAME") || "demo";
const mongoUri = dburi || `mongodb://${dbhost}:${dbport}`;
const client = new MongoClient(mongoUri);

const port: number = Number(Deno.env.get("RUN_PORT")) || 3000;
const app = express();

async function connectMongoDB() {
  try {
    await client.connect();
    console.log("Connected to MongoDB");
  } catch (err) {
    console.error("Error connecting with MongoDB", err.message);
  }
}

connectMongoDB();

app.get("/", (req: Request, res: Response) => {
  return res.send("Hi there!");
});

app.get("/health", (req: Request, res: Response) => {
  return res.status(200).json({ status: "healthy" });
});

app.get("/data", async (req: Request, res: Response) => {
  try {
    const db = client.db(dbname);
    const collection = db.collection("messages");

    const data = { name: "john", message: "Ok" };
    await collection.updateOne({ name: data.name }, { $setOnInsert: data }, { upsert: true });

    const messages = await collection.find({}).toArray();

    return res.json(messages);
  } catch (err) {
    console.error("Error getting data", err);
    return res.status(500).json({ error: "Error getting data" });
  }
});

app.listen(port, () => {
  console.log(`Server listening on ${port}`);
});

Nótese que se importa MongoClient, se configura conexión y se establece con la función connectMongoDB. Posteriormente se implementa lógica de consulta en el bloque de la ruta /data (agregando al menos un registro de ejemplo)

Contenirizando una aplicación con Deno

Si queremos crear un imágen de contenedor para este programa o servicio, podemos abrir un archivo Dockerfile colocando un código como el siguiente:

FROM denoland/deno:2.0.2
EXPOSE 3000
WORKDIR /app
USER deno
COPY . .
RUN deno cache main.ts
CMD ["run", "--allow-net", "--allow-read", "--allow-env", "--allow-sys", "main.ts"]

Dado el ejercicio logrado hasta ahora, antes de construir la imágen podemos revisar que quede operando un contenedor con la instancia de la base de datos MongoDB, por ejemplo, ejecutando lo siguiente:

docker network create denoapp
docker run --name mongodb --network denoapp \
-p 27027:27017 \
-v mongodb:/data/db -v mongocfg:/data/configdb \
-d mongo:8.0

Nótese que se crea una red (con --network, y sería de tipo bridge) buscando conectividad, pero puede usarse la red por defecto.
Si estas usando Podman en lugar de Docker, se reemplaza el comando docker por podman.

Procedemos a construir la imagen de nuestro programa y lanzamos un contenedor con los siguientes comandos:

docker build -t denoapp:v1 .
docker run --name denoapp --network denoapp -e DB_HOST=mongodb -e DB_PORT=27017 -e DB_USER=root -e DB_PASS=password -p 3000:3000 -d denoapp:v1

Nótese que se pasan los parámetros usados para conectar con MongoDB en la red interna. Por ejemplo, mongodb (el nombre del contenedor) en lugar de localhost y puerto 27017 en lugar de 27027 (además usuario y password según la imágen de MongoDB)

Para subir una imágen se puede utilizar el servicio AWS ECR, por ejemplo, podríamos ejecutar algo como lo siguiente:

git clone https://github.com/kaesar/denoapp.git
docker build -t denoapp:v1 .

aws ecr create-repository --repository-name denoapp --region us-east-1
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

docker tag denoapp:v1 123456789012.dkr.ecr.us-east-1.amazonaws.com/denoapp:v1
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/denoapp:v1

Los comandos anteriores simplemente ilustran que se construye una imágen y se sube a un registro de repositorio de imágenes (AWS ECR).
docker tag asigna un alias de etiqueta para preparar la imágen cuando se suba (coincidiendo así con el repositorio remoto), y docker push sube la imágen etiquetada recientemente.