Crear una pequeña API de Deno

En esta publicación, me gustaría contar y mostrar el proceso de creación de una pequeña API con Deno. Deno es el último lanzador Javascript y mecanografiado desarrollado por el creador de Node.js, Ryan Dahl.



Nuestros objetivos :

  • Desarrolle una API que funcione con los datos del usuario.
  • Proporcione la capacidad de usar los métodos GET, POST, PUT y DELETE
  • Guarde y actualice los datos del usuario en el archivo JSON local
  • Use el marco para acelerar el desarrollo

Lo único que necesitamos instalar es Deno. Deno admite Typecript desde el primer momento. Para este ejemplo, utilicé Deno versión 0.22 y este código puede no funcionar en futuras versiones.
La versión del Deno instalado se puede encontrar con el comando deno version en el terminal.

Estructura del programa


handlers
middlewares
models
services
config.ts
index.ts
routing.ts

Como puede ver, parece una pequeña aplicación web en Node.js.

  • manejadores - contiene manejadores de ruta
  • middlewares : contiene funciones que se iniciarán en cada solicitud
  • modelos : contiene la designación de modelos, en nuestro caso es solo una interfaz de usuario
  • servicios - contiene ... servicios!
  • config.ts - archivo de configuración de la aplicación
  • index.ts es el punto de entrada para nuestra aplicación
  • routing.ts : contiene rutas API

Selección de marco


Hay muchos marcos excelentes para Node.js. Uno de los más populares es Express . También hay una versión moderna de Express'a - Koa . Desafortunadamente, Deno no es compatible con las bibliotecas Node.js y la elección es mucho más pequeña, pero hay un marco para Deno, que se basa en Koa - Oak . Lo usaremos para nuestro pequeño proyecto. Si nunca ha usado Koa, no se preocupe, se parece mucho a Express.

Crear un punto de entrada a la aplicación


index.ts

import { Application } from "https://deno.land/x/oak/mod.ts";
import { APP_HOST, APP_PORT } from "./config.ts";
import router from "./routing.ts";
import notFound from "./handlers/notFound.ts";
import errorMiddleware from "./middlewares/error.ts";

const app = new Application();

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

console.log(`Listening on ${APP_PORT}...`);

await app.listen(`${APP_HOST}:${APP_PORT}`);

En la primera línea, utilizamos uno de los principales chips de Deno: importar módulos directamente desde Internet. Después de eso, no hay nada inusual: creamos una aplicación, agregamos middleware, rutas y, finalmente, iniciamos el servidor. Todo es igual que cuando se usa Express o Koa.

Crear configuración


config.ts
const env = Deno.env();
export const APP_HOST = env.APP_HOST || "127.0.0.1";
export const APP_PORT = env.APP_PORT || 4000;
export const DB_PATH = env.DB_PATH || "./db/users.json";

Nuestro archivo de configuración. La configuración se transfiere desde el entorno de inicio, pero también agregaremos valores predeterminados. La función Deno.env () es un análogo de process.env en Node.js.

Agregar un modelo de usuario


modelos / usuario.ts

export interface User {
  id: string;
  name: string;
  role: string;
  jiraAdmin: boolean;
  added: Date;
}

Crear un archivo de ruta


routing.ts

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

import getUsers from "./handlers/getUsers.ts";
import getUserDetails from "./handlers/getUserDetails.ts";
import createUser from "./handlers/createUser.ts";
import updateUser from "./handlers/updateUser.ts";
import deleteUser from "./handlers/deleteUser.ts";

const router = new Router();

router
  .get("/users", getUsers)
  .get("/users/:id", getUserDetails)
  .post("/users", createUser)
  .put("/users/:id", updateUser)
  .delete("/users/:id", deleteUser);

export default router;

De nuevo, nada inusual. Creamos un enrutador y le agregamos varias rutas. Parece casi como si hubieras copiado código de una aplicación Express, ¿verdad?

Manejo de eventos para rutas


handlers / getUsers.ts

import { getUsers } from "../services/users.ts";

export default async ({ response }) => {
  response.body = await getUsers();
};

Devuelve todos los usuarios. Si nunca has usado Koa, te lo explicaré. El objeto de respuesta es análogo a res en Express. El objeto res en Express tiene un par de métodos como json o send , que se utilizan para enviar una respuesta. En Oak y Koa, debemos establecer el valor que queremos devolver a la propiedad response.body .

manejadores / getUserDetails.ts

import { getUser } from "../services/users.ts";

export default async ({ params, response }) => {
  const userId = params.id;

  if (!userId) {
    response.status = 400;
    response.body = { msg: "Invalid user id" };
    return;
  }

  const foundUser = await getUser(userId);
  if (!foundUser) {
    response.status = 404;
    response.body = { msg: `User with ID ${userId} not found` };
    return;
  }

  response.body = foundUser;
};

Aquí todo es fácil también. El controlador devuelve al usuario con el Id deseado.

manejadores / createUser.ts

import { createUser } from "../services/users.ts";

export default async ({ request, response }) => {
  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: "Invalid user data" };
    return;
  }

  const {
    value: { name, role, jiraAdmin }
  } = await request.body();

  if (!name || !role) {
    response.status = 422;
    response.body = { msg: "Incorrect user data. Name and role are required" };
    return;
  }

  const userId = await createUser({ name, role, jiraAdmin });

  response.body = { msg: "User created", userId };
};

Este controlador es responsable de crear el usuario.

manejadores / updateUser.ts

import { updateUser } from "../services/users.ts";

export default async ({ params, request, response }) => {
  const userId = params.id;

  if (!userId) {
    response.status = 400;
    response.body = { msg: "Invalid user id" };
    return;
  }

  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: "Invalid user data" };
    return;
  }

  const {
    value: { name, role, jiraAdmin }
  } = await request.body();

  await updateUser(userId, { name, role, jiraAdmin });

  response.body = { msg: "User updated" };
};

El controlador verifica si el usuario con el ID especificado existe y actualiza los datos del usuario.

manejadores / deleteUser.ts

import { deleteUser, getUser } from "../services/users.ts";

export default async ({ params, response }) => {
  const userId = params.id;

  if (!userId) {
    response.status = 400;
    response.body = { msg: "Invalid user id" };
    return;
  }

  const foundUser = await getUser(userId);
  if (!foundUser) {
    response.status = 404;
    response.body = { msg: `User with ID ${userId} not found` };
    return;
  }

  await deleteUser(userId);
  response.body = { msg: "User deleted" };
};

Responsable de eliminar un usuario.

También es aconsejable procesar solicitudes de rutas inexistentes y devolver un mensaje de error.

handlers / notFound.ts

export default ({ response }) => {
  response.status = 404;
  response.body = { msg: "Not Found" };
};

Agregar servicios


Antes de crear servicios que funcionen con los datos del usuario, necesitamos hacer dos pequeños servicios auxiliares.

servicios / createId.ts

import { v4 as uuid } from "https://deno.land/std/uuid/mod.ts";

export default () => uuid.generate();

Cada nuevo usuario recibirá una identificación única. Utilizaremos el módulo uuid de la biblioteca estándar de Deno para generar un número aleatorio.

servicios / db.ts

import { DB_PATH } from "../config.ts";
import { User } from "../models/user.ts";

export const fetchData = async (): Promise<User[]> => {
  const data = await Deno.readFile(DB_PATH);

  const decoder = new TextDecoder();
  const decodedData = decoder.decode(data);

  return JSON.parse(decodedData);
};

export const persistData = async (data): Promise<void> => {
  const encoder = new TextEncoder();
  await Deno.writeFile(DB_PATH, encoder.encode(JSON.stringify(data)));
};

Este servicio ayudará a interactuar con el archivo JSON en el que se almacenarán los datos del usuario.
Para obtener todos los usuarios, lea el contenido del archivo. La función readFile devuelve un objeto de tipo Uint8Array , que debe convertirse a un tipo de cadena antes de ingresar en un archivo JSON .

Y finalmente, el servicio principal para trabajar con datos de usuario.

servicios / usuarios.ts

import { fetchData, persistData } from "./db.ts";
import { User } from "../models/user.ts";
import createId from "../services/createId.ts";

type UserData = Pick<User, "name" | "role" | "jiraAdmin">;

export const getUsers = async (): Promise<User[]> => {
  const users = await fetchData();

  // sort by name
  return users.sort((a, b) => a.name.localeCompare(b.name));
};

export const getUser = async (userId: string): Promise<User | undefined> => {
  const users = await fetchData();

  return users.find(({ id }) => id === userId);
};

export const createUser = async (userData: UserData): Promise<string> => {
  const users = await fetchData();

  const newUser: User = {
    id: createId(),
    name: String(userData.name),
    role: String(userData.role),
    jiraAdmin: "jiraAdmin" in userData ? Boolean(userData.jiraAdmin) : false,
    added: new Date()
  };

  await persistData([...users, newUser]);

  return newUser.id;
};

export const updateUser = async (
  userId: string,
  userData: UserData
): Promise<void> => {
  const user = await getUser(userId);

  if (!user) {
    throw new Error("User not found");
  }

  const updatedUser = {
    ...user,
    name: userData.name !== undefined ? String(userData.name) : user.name,
    role: userData.role !== undefined ? String(userData.role) : user.role,
    jiraAdmin:
      userData.jiraAdmin !== undefined
        ? Boolean(userData.jiraAdmin)
        : user.jiraAdmin
  };

  const users = await fetchData();
  const filteredUsers = users.filter(user => user.id !== userId);

  persistData([...filteredUsers, updatedUser]);
};

export const deleteUser = async (userId: string): Promise<void> => {
  const users = await getUsers();
  const filteredUsers = users.filter(user => user.id !== userId);

  persistData(filteredUsers);
};

Aquí hay un montón de código, pero es puramente mecanografiado.

Error al procesar


¿Qué podría ser peor que lo que sucedería en caso de un error de servicio al trabajar con los datos del usuario? Todo el programa puede fallar. Para evitar este escenario, puede usar la construcción try / catch en cada controlador. Pero hay una solución más elegante: agregue middleware frente a cada ruta y evite cualquier error inesperado que pueda ocurrir.

middlewares / error.ts

export default async ({ response }, next) => {
  try {
    await next();
  } catch (err) {
    response.status = 500;
    response.body = { msg: err.message };
  }
};

Antes de comenzar el programa en sí, necesitamos agregar datos para ejecutar.

db / users.json

[
  {
    "id": "1",
    "name": "Daniel",
    "role": "Software Architect",
    "jiraAdmin": true,
    "added": "2017-10-15"
  },
  {
    "id": "2",
    "name": "Markus",
    "role": "Frontend Engineer",
    "jiraAdmin": false,
    "added": "2018-09-01"
  }
]

¡Eso es todo! Ahora puede intentar ejecutar nuestra aplicación:

deno -A index.ts

el indicador "A" significa que no es necesario que el programa tenga permisos por separado. No olvide que usar esta bandera no es seguro en la producción.

Lo más probable es que vea muchas líneas con descarga (Descarga) y compilación (Compilación). Al final, la línea codiciada debería aparecer:

Listening on 4000

Para resumir


¿Qué herramientas utilizamos?

  1. Deno objeto global para leer / escribir archivos
  2. uuid de la biblioteca estándar de Deno para crear una identificación única
  3. roble : un marco de terceros inspirado en Koa para Node.js
  4. Objetos puramente mecanografiados, TextEncode o JSON incluidos en Javascript

¿Cuál es la diferencia de Node.js?


  • No es necesario instalar y configurar el compilador para Typescript u otras herramientas como ts-node. Simplemente puede ejecutar el programa con el comando deno index.ts
  • Inclusión de todos los módulos de terceros directamente en el código sin la necesidad de una instalación preliminar
  • Falta de package.json y package-lock.json
  • La ausencia de node_modules en el directorio raíz de nuestro programa. Todos los archivos descargados se encuentran en la caché global

Si es necesario, el código fuente se puede encontrar aquí .

Source: https://habr.com/ru/post/undefined/


All Articles