DEV Community

Cover image for Iniciando con Deno - API Rest
Juan Gajardo
Juan Gajardo

Posted on

Iniciando con Deno - API Rest

La primera versión de Deno fue lanzada el 13 de mayo del 2020. Este Post esta enfocado en ser una guía para aquellos que estamos aprendiendo Deno, para ello realizaremos una API Rest con este interesante Javascript desde el servidor.

Que es Deno?

Como lo dice en su propio sitio web deno.land, es un simple, moderno y seguro runtime para Javascript y Typescript que usa el motor V8 de Chrome y esta construido con Rust.

Que vamos a realizar?

Comenzaremos con la instalación de Deno en windows, para luego crear una API Rest con Deno basado en el clásico patrón de diseño MVC (modelo-vista-controlador) con el framework "Oak", la capa de "vista" la dejaremos para otro post donde conectaremos nuestra Api Deno con un front realizado con React JS.

Estructura de la aplicación:

estructura-api-deno

Contenido:

  • Login con auth middleware (JWT)
  • CRUD con modelo (interface) Car y User
  • Creación de auth middleware
  • Configuración de drun para el reinicio automático del servidor

Instalación

Deno es de simple instalación, solo debemos abrir "PowerShell" y ejecutar el siguiente comando:

iwr https://deno.land/x/install/install.ps1 -useb | iex

Obtendremos el siguiente resultado:

Deno was installed successfully to C:\Users\carlo\.deno\bin\deno.exe
Run 'deno --help' to get started

Luego podemos ejecutar el siguiente comando para ver el "Hola Mundo" que nos provee el mismo Deno:

deno run https://deno.land/std/examples/welcome.ts

El cual nos mostrara lo siguiente:

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

Y listo ya tenemos instalado Deno en nuestro windows, para poder instalar en linux o mac deben realizar algo similar, para ello ejecutar los comandos que nos indica la documentación oficial en el siguiente link: deno instalación.

Archivo server.ts el inicio de nuestra app

Este archivo es que inicializara nuestro servidor web, para ello implementaremos el framework "oak" de deno:

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

//Set Host and Port
const HOST = "127.0.0.1";
const PORT = 4000;

const app = new Application();

// Logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.headers.get("X-Response-Time");
});

// Timing
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});

console.log(`Welcome API Deno :) ${HOST}:${PORT}...`);
await app.listen(`${HOST}:${PORT}`);


Al principio de nuestro código llamamos a Application de oak, esto nos servirá para poder iniciar nuestro servidor deno, luego seteamos el host y el puerto, que en nuestro caso serán localhost y 4000 respectivamente. Después realizamos la configuración de nuestro logger mas el timing de los request que hagamos a nuestra aplicación:

POST http://localhost:4000/users/login - 4ms

También realizamos un console.log de un welcome a la api y iniciamos al final nuestra app, nos debería entregar por consola lo siguiente:

Welcome API Deno :) 127.0.0.1:4000...

Drun y config.ts

Drun es una librería que nos ayuda a reiniciar nuestra aplicación mientras estamos desarrollando, para los que hemos desarrollado con node js, es similar a lo que hace Nodemon.

Para instalar drun es necesario ejecutar el siguiente comando en la consola:

deno install --allow-read --allow-run --unstable https://deno.land/x/drun@v1.1.0/drun.ts

Luego creamos un archivo en la raiz llamado drun.json y colocamos el siguiente código:

{
  "entryPoint": "./server.ts",
  "cwd": "./",
  "excludes": [""],
  "runtimeOptions": ["--allow-net", "--allow-read"]
}

Para iniciar nuestro proyecto con drun es necesario ejecutar el comando drun y listo, ya podemos trabajar sin preocuparnos de reiniciar el servidor de forma manual.

También crearemos en la raíz de nuestro proyecto, el archivo config.ts y colocaremos el siguiente código:

export const key = "your-secret-key";

Esta sera nuestra "key" para después configurar nuestro JWT.

Definición de modelos (interfaces o entidades)

Utilizaremos arreglos de objetos para emular nuestra base de datos, para que el post no sea demasiado extenso.

Estos modelos son los que representan a una entidad de la base de datos, estos los vamos a tener dentro de la carpeta "types" de nuestra aplicación:

/types/user.ts

export interface User {
  id: string;
  username: string;
  password: string;
}

/types/car.ts

export interface Car {
  id: string;
  model: string;
  price: number;
}


Controladores

El controlador es el encargado de interactuar entre nuestra entidad y la vista.

/controllers/car.ts

import { Car } from "../types/car.ts";

//Array cars
let cars: Array<Car> = [
  {
    id: "1",
    model: "Kia Morning",
    price: 5490990,
  },
  {
    id: "2",
    model: "Kia Cerato",
    price: 10990990,
  },
  {
    id: "3",
    model: "Kia Sportage",
    price: 14990990,
  },
  {
    id: "4",
    model: "Kia Stinger",
    price: 29990990,
  },
  {
    id: "5",
    model: "Kia Rio",
    price: 7990990,
  },
];

//Return all cars from databases
const getCars = ({ response }: { response: any }) => {
  response.body = cars;
};

//Return car by id
const getCar = ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  const car = cars.filter((car) => car.id == params.id)[0];
  if (car) {
    response.status = 200;
    response.body = car;
  } else {
    response.status = 404;
    response.body = { message: "404 Not found" };
  }
};

//Creates new car
const createCar = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) => {
  const body = await request.body();
  const car: Car = body.value;
  cars.push(car);
  response.body = { success: true, data: car };
  response.status = 201;
};

//Update existing car
const updateCar = async ({
  params,
  request,
  response,
}: {
  params: { id: string };
  request: any;
  response: any;
}) => {
  const car = cars.filter((car) => car.id == params.id)[0];
  if (cars) {
    const body = await request.body();
    car.model = body.value.model;
    car.price = body.value.price;
    response.status = 200;
    response.body = {
      success: true,
      data: cars,
    };
  } else {
    response.status = 404;
    response.body = {
      success: false,
      message: "Car not found",
    };
  }
};

//Delete car
const deleteCar = ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  cars = cars.filter((car) => car.id !== params.id);
  response.status = 200;
  response.body = { success: true, message: "Car removed" };
};

export { getCars, getCar, createCar, updateCar, deleteCar };


En este controlador, llamamos a nuestra entidad Car para luego crear nuestro array de automóviles. Luego realizamos los siguientes métodos CRUD:

  • getCars -> Traer todos los automóviles
  • getCar -> Traer automóvil por su id
  • createCar -> Crear nuevo automóvil
  • updateCar -> Actualizar automóvil por su id y parámetros
  • deleteCar -> Eliminar automóvil por su id

Es importante mencionar que este controlador esta realizado sin el framework Oak, a diferencia del controlador a continuación, que si utiliza este framework, esto mas que nada para poder ver como seria solo con deno y también con oak.

/controllers/user.ts

import { User } from "../types/user.ts";
import { Context } from "https://deno.land/x/oak/mod.ts";

import {
  makeJwt,
  setExpiration,
  Jose,
  Payload,
} from "https://deno.land/x/djwt/create.ts";

import { key } from "../config.ts";

//Array users
let users: Array<User> = [
  {
    id: "1",
    username: "jdoe@example.com",
    password: "1234",
  },
  {
    id: "2",
    username: "jdae@example.com",
    password: "4321",
  },
];

const header: Jose = {
  alg: "HS256",
  typ: "JWT",
};

//login user
const login = async (ctx: Context) => {
  //get values from body request
  const { value } = await ctx.request.body();
  //iterate array users

  const user = users.filter((x) => x.username === value.username);

  if (user.length != 0) {
    if (
      user[0].username === value.username &&
      user[0].password === value.password
    ) {
      const payload: Payload = {
        iss: user[0].username,
        exp: setExpiration(new Date().getTime() + 50000),
      };

      //create jwt previous condition ok
      const jwt = makeJwt({ key, header, payload });
      if (jwt) {
        // response jwt
        ctx.response.status = 200;
        ctx.response.body = {
          id: user[0].id,
          username: user[0].username,
          jwt,
        };
      } else {
        // if error, response code 500
        ctx.response.status = 500;
        ctx.response.body = {
          message: "Internal error server",
        };
      }
      return;
    } else {
      //credentials wrong
      ctx.response.status = 422;
      ctx.response.body = {
        message: "Invalid username or password",
      };
    }
  } else {
    //user not found in db
    ctx.response.status = 400;
    ctx.response.body = {
      message: "User not found in database",
    };
  }
};

export { login };

Iniciamos llamando a la entidad User para poder utilizarla, luego importamos los diferentes métodos que necesitamos para la generación del token, desde la librería djwt. Importamos la key desde el archivo config.ts que tenemos en la raíz de nuestro proyecto. Luego procedemos a crear nuestro array de usuarios.

Creamos nuestro método login, es importante mencionar que en este controlador hacemos uso de Context de Oak para manipular los request y response. Sacamos los values del request y realizamos un filter para poder ver si el usuario existe, en el caso que no, enviamos una respuesta 400 indicando en un message que el usuario no existe en nuestra base de datos. En caso que si exista, realizamos la comparación de la contraseña, si no coincide, entregamos una respuesta 422 con un mensaje de credenciales erróneas, en caso que si coincide procedemos a crear el token para responder en un objeto, el id, username y el jwt.

Rutas

Crearemos un archivo router para poder manejar las rutas de nuestra aplicación de manera simple. Para esto creamos la carpeta router y dentro el archivo router.ts:

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

import {
  getCars,
  getCar,
  createCar,
  updateCar,
  deleteCar,
} from "../controllers/car.ts";

import { login } from "../controllers/user.ts";

import { authMiddleware } from "../middlewares/auth.ts";

const router = new Router();

router
  .post("/users/login", login)
  .get("/cars", authMiddleware, getCars)
  .get("/car/:id", authMiddleware, getCar)
  .post("/cars", authMiddleware, createCar)
  .put("/cars/:id", authMiddleware, updateCar)
  .delete("/cars/:id", authMiddleware, deleteCar);

export default router;

Partimos llamando a Router desde Oak para poder crear el router, luego importamos nuestros métodos CRUD desde el controlador car y también método login desde el controlador user. En este punto también traemos algo nuevo, nuestro authMiddleware para poder trabajar nuestros métodos solicitando JWT, veremos como desarrollarlo en la siguiente sección. Después mapeamos nuestras peticiones: GET, POST, PUT y DELETE, e indicando donde necesitamos que actué nuestro authMiddleware, en este caso dejaremos todos los métodos protegidos, a excepción de /users/login que sera publico, finalmente exportamos nuestro router. Ya con nuestro archivo listo podemos modificar nuestro archivo server.ts para agregar las rutas a nuestra aplicación:

/server.ts

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

const HOST = "127.0.0.1";
const PORT = 4000;
const app = new Application();

// Logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.headers.get("X-Response-Time");
  console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
});

// Timing
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});

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

console.log(`Welcome API Deno :) ${HOST}:${PORT}...`);
await app.listen(`${HOST}:${PORT}`);

De esta forma integramos las rutas a nuestro archivo server.ts.

Middlewares

Crearemos un middleware el cual nos servirá para proteger nuestras rutas y así puedan acceder a los datos solo los usuarios registrados en nuestra aplicación. Comenzamos creando una carpeta en la raíz /middlewares para luego crear el archivo:

/middlewares/auth.ts

import { Context } from "https://deno.land/x/oak/mod.ts";
import { validateJwt } from "https://deno.land/x/djwt/validate.ts";

import { key } from "../config.ts";

const authMiddleware = async (ctx: Context, next: any) => {
  const headers: Headers = ctx.request.headers;
  const authorization = headers.get("Authorization");
  if (!authorization) {
    ctx.response.status = 401;
    ctx.response.body = { message: "Header : Authorization" };
    return;
  }
  const jwt = authorization.split(" ")[1];
  if (!jwt) {
    ctx.response.status = 401;
    ctx.response.body = { message: "JWT is necessary" };
    return;
  }
  if (await validateJwt(jwt, key, { isThrowing: false })) {
    await next();
    return;
  }

  ctx.response.status = 401;
  ctx.response.body = { message: "Invalid jwt token" };
};

export { authMiddleware };


Iniciamos el archivo, trayendo a Context desde Oak, para trabajar con request y response. Luego traemos validateJwt desde djwt, en este caso authMiddleware recibe como parámetro context y next, por convención se utiliza next y básicamente este es el argumento para dar paso a la siguiente función post-middleware. Capturamos headers desde el request utilizando context para obtener la cabecera "Authorization", despues validamos si existe esta cabecera, de no ser así, enviamos una respuesta código 401 indicando que es necesaria. Luego utilizamos la función split() para poder obtener el token:

*El token desde el cliente y en la cabecera Authorization deberia venir con la siguiente configuración Bearer ${token}, básicamente split divide esta cadena donde encuentra un espacio, el [1] equivale al nuevo array que genera split, quedando en la posición [0] Bearer y en la posición [1] el token que necesitamos.

Después validamos si es que hay token, si no hay, enviamos una respuesta 401 indicando que el JWT es necesario, en caso contrario, validamos el token con nuestra función validateJwt() y aplicamos next() para que de esta forma, el flujo pueda seguir hacia la siguiente función:

.get("/cars", authMiddleware, getCars) en este ejemplo, para que continué con getCars

De lo contrario, enviamos una respuesta 401 indicando que el JWT es invalido, por que por ejemplo, este ya expiro.

Conclusión

De esta forma, podemos tener nuestra API Rest con Deno y una estructura muy similar a como se trabajaría en Node JS. Es importante mencionar que Deno utilice nativamente typescript, da una mayor facilidad a la detección de errores, sobre todo de "tipos de datos", agilizando el proceso de desarrollo ya que podemos detectar errores tempranamente. Otra característica interesante es su "Standard Library" que trae por defecto, y la descentralización de las librerías de terceros creando un cache con esta, para no llamarlas en cada momento.

Les dejo el código en el siguiente repositorio de gitlab: Iniciando con Deno - API Rest

Este es el primer post que hago, cualquier feedback que quieran dejar para mejorar en los siguientes post que vendrán, es totalmente bienvenida.

Los 2 siguientes post en los que estaré trabajando seran:

*Documentación de nuesta API con POSTMAN
*Creación de front-end con React (Hooks) consumiendo nuestra API REST.

Saludos y hasta luego.

Discussion (3)

Collapse
miguelcolon profile image
miguelC-olon

Hi, I have the following error when trying to install Drun:

error: TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'.
Type 'URL' is not assignable to type 'string'.
return new URL(url).pathname
~~~
at deno.land/std@v0.54.0/path/win32.t...

TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'.
Type 'URL' is not assignable to type 'string'.
return new URL(url).pathname;
~~~
at deno.land/std@v0.54.0/path/posix.t...

Found 2 errors.

Collapse
miguelcolon profile image
miguelC-olon

Hola, como se corre la solucion final?, pues al hacerlo con:

deno run -A server.ts

pero tira un error sobre el login:

Check server.ts
error: TS2532 [ERROR]: Object is possibly 'undefined'.
const user = users.filter((x) => x.username === value.username);
~~~~~
at /controllers/user.ts:38:51

TS2339 [ERROR]: Property 'username' does not exist on type 'Promise | Promise | FormDataReader | Promise | Promise'.
Property 'username' does not exist on type 'Promise'.
const user = users.filter((x) => x.username === value.username);
~~~~~~~~
at /controllers/user.ts:38:57

Collapse
juangaj07538514 profile image
Juan Gajardo Author

Hola Miguel, la solución final basta con solo ejecutar el comando "drun" y listo.