Así puedes crear tu primera API REST con Deno

No es necesario explicar los pasos para instalarlo. Están muy bien descritos en la web oficial. Yo para este ejemplo he usado VS Code, para el que existe este plugin oficial.

¿Cómo es Deno?

Deno es muy parecido a Node, pero lo que intenta hacer es ser más bueno construyendo un runtime más seguro y corrigiendo los errores de base que tenía Node. Deno es seguro por defecto. Esto significa que no tiene acceso a tu disco duro, ni a tu red. Lo tendrá únicamente si tú se lo das. Node, en cambio, tiene acceso a prácticamente todo nada más instalarlo.

Por ejemplo, si seguimos la guía oficial y ejecutamos el primer comando, veremos que funciona sin problema.

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

Pero si lo hacemos con el ejemplo que viene a continuación:

import { serve } from "https://deno.land/[email protected]/http/server.ts";
const s = serve({ port: 8000 });

console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

Nos encontraremos con un error de permisos de acceso a la red:

Compile file://welcome.ts
error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10)
    at Object.listen ($deno$/ops/net.ts:51:10)
    at listen ($deno$/net.ts:152:22)
    at serve (https://deno.land/[email protected]/http/server.ts:261:20)
    at file://welcome.ts:2:11

Esto sucede porque el primer ejemplo no necesita ningún tipo de acceso adicional para funcionar, pero el segundo necesita acceso a nuestra red. Deno, al tener seguridad por defecto, no nos permite hacerlo a no ser que le otorguemos permiso para ello.

En este ejemplo, basta con ejecutarlo con el parámetro –allow-net, y funcionará.

Lo mismo ocurre con los ficheros, en este caso necesitamos –allow-read

Crear una API con Deno

Qué mejor forma para empezar a jugar con Deno que creando nuestra primera API REST.

Con este pequeño tutorial voy a crear un array muy simple de películas y los 5 métodos para listar, buscar, crear, actualizar y eliminar elementos.

El primer paso es crear un fichero de arranque. En este caso app.ts. Lo primero será cargar Oak, un framework middleware para el servidor http de Deno. Oak está inspirado en Koa, un middleware para Node.js. Parece ser que siguen con el juego de palabras. Al final, nos ayuda a que escribir APIs sea más sencillo.

Es un ejemplo bastante sencillo que se explica prácticamente por si solo. El servidor escuchará por el puerto 4000 y cargará las rutas definidas en el fichero router.ts que veremos justo después. En el fichero ./api/controller.ts pondré la definición de las funciones para los distintos endpoints.

import { Application } from "https://deno.land/x/oak/mod.ts";
import router from "./router.ts";
import {
  getMovies,
  getMovie,
  createMovie,
  updateMovie,
  deleteMovie,
} from "./api/controller.ts";

const env = Deno.env.toObject();
const HOST = env.HOST || "127.0.0.1";
const PORT = env.PORT || 4000;

const app = new Application();

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

console.log(`API is listening on ${HOST}:${PORT}...`);
await app.listen(`${HOST}:${PORT}`);

Momento de definir las rutas en el fichero router.ts. Aquí importaremos también el Router de Oak y las definiciones que crearemos en el controller.ts

Instanciamos un Router y definimos las 5 rutas comentadas.

MétodoFunción
getMoviesDevuelve todas las películas
getMovieDevuelve una película a partir de un id
createMovieCrea una nueva película
updateMovieActualiza una película ya existente
deleteMovieElimina una película
import { Router } from "https://deno.land/x/oak/mod.ts";
import {
  getMovies,
  getMovie,
  createMovie,
  updateMovie,
  deleteMovie,
} from "./api/controller.ts";

const router = new Router();

router
  .get("/movies", getMovies)
  .get("/movie/:id", getMovie)
  .post("/movies", createMovie)
  .put("/movies/:id", updateMovie)
  .delete("/movies/:id", deleteMovie);

export default router;

Ahora es momento de crear el fichero controller.ts para definir los métodos de la API y el array con la base de datos de prueba.

interface Movie {
  id: string;
  title: string;
  rating: number;
}

Justo a continuación, creamos el array comentado:

/**
 * Sample array with movies
 */
let movies: Array<Movie> = [
  {
    id: "1",
    title: "TENET",
    rating: 10,
  },
  {
    id: "2",
    title: "No Time to Die",
    rating: 8,
  },
  {
    id: "3",
    title: "The Way Back",
    rating: 7,
  },
  {
    id: "4",
    title: "The Invisible Man",
    rating: 9,
  },
  {
    id: "5",
    title: "Onward",
    rating: 8,
  },
];

Y ahora los distintos métodos, empezando por el que lista todas las películas. Quedaría así de simple:

/**
 * Returns all the movies in database
 */
const getMovies = ({ response }: { response: any }) => {
  response.body = movies;
};

Vamos a por el siguiente, el encargado de devolver una película a partir de un ID que le podremos pasar por parámetro.

/**
 * Returns a movie by id
 */
const getMovie = ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  const movie = movies.filter((movie) => movie.id == params.id)[0];
  if (movie) {
    response.status = 200;
    response.body = movie;
  } else {
    response.status = 404;
    response.body = { message: "404 Not found" };
  }
};

Si probamos a lanzar la petición con Postman, veremos que funciona.

image 12
Lanzamos la petición para un ID en concreto.

Le toca el turno al método createMovie para crear una película. El código es el siguiente:

/**
 * Creates a new movie
 */
const createMovie = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) => {
  const body = await request.body();
  const movie: Movie = body.value;
  movies.push(movie);
  response.body = { success: true, data: movie };
  response.status = 201;
};

Si lanzamos la petición de prueba, el servidor nos contestará con el mensaje programado.

image 14

Si a continuación lanzamos la petición para devolver todas las películas, veremos como aparece la nueva correctamente.

image 15

Es el turno del método updateMovie para actualizar una película. El código es:

/**
 * Updates an existing movie
 */
const updateMovie = async ({
  params,
  request,
  response,
}: {
  params: { id: string };
  request: any;
  response: any;
}) => {
  const movie = movies.filter((movie) => movie.id == params.id)[0];
  if (movie) {
    const body = await request.body();
    movie.title = body.value.title;
    movie.rating = body.value.rating;
    response.status = 200;
    response.body = {
      success: true,
      data: movies,
    };
  } else {
    response.status = 404;
    response.body = {
      success: false,
      message: "Movie not found",
    };
  }
};

Lanzamos la correspondiente petición PUT con Postman, y obtendremos la respuesta correcta.

image 17

Y para finalizar, solo nos queda el método deleteMovie que, en este caso, elimina una película a partir de un id. Lo que hago es utilizar el filter() para actualizar el array manteniendo todas las películas con id distinto al enviado.

/**
 * Deletes a movie by a given id
 */
const deleteMovie = ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  movies = movies.filter((movie) => movie.id !== params.id);
  response.status = 200;
  response.body = { success: true, message: "Movie removed" };
};

Probamos con Postman…

image 19

Y efectivamente acaba de desaparecer la película con id = 1.

image 21

Puedes descargarte todo el código de este ejemplo en este repositorio de mi GitHub.