Criando uma pequena API Deno

Neste post, eu gostaria de contar e mostrar o processo de criação de uma pequena API usando o Deno. Deno é o mais recente lançador Javascript e TypeScript desenvolvido pelo criador do Node.js. Ryan Dahl.



Nossos objetivos :

  • Desenvolver uma API que funcione com dados do usuário
  • Fornecer a capacidade de usar os métodos GET, POST, PUT e DELETE
  • Salvar e atualizar dados do usuário no arquivo JSON local
  • Use a estrutura para acelerar o desenvolvimento

A única coisa que precisamos instalar é o Deno. Deno suporta o Typecript imediatamente. Neste exemplo, usei o Deno versão 0.22 e esse código pode não funcionar em versões futuras.
A versão do Deno instalada pode ser encontrada com o comando deno version no terminal.

Estrutura do programa


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

Como você pode ver, parece um pequeno aplicativo da Web no Node.js.

  • manipuladores - contém manipuladores de rota
  • middlewares - contém funções que serão lançadas a cada solicitação
  • models - contém a designação de modelos; no nosso caso, é apenas uma interface do usuário
  • serviços - contém ... serviços!
  • config.ts - arquivo de configuração do aplicativo
  • index.ts é o ponto de entrada para nossa aplicação
  • routing.ts - contém rotas da API

Seleção de framework


Existem muitas estruturas excelentes para o Node.js. Um dos mais populares é o Express . Existe também uma versão moderna do Express'a - Koa . Infelizmente, o Deno não suporta bibliotecas Node.js. A escolha é muito menor, mas existe uma estrutura para o Deno, baseada no Koa- Oak . Vamos usá-lo para o nosso pequeno projeto. Se você nunca usou o Koa, não se preocupe, ele se parece muito com o Express.

Criando um ponto de entrada do aplicativo


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}`);

Na primeira linha, usamos um dos principais chips Deno - importando módulos diretamente da Internet. Depois disso, não há nada incomum: criamos um aplicativo, adicionamos middleware, rotas e, finalmente, iniciamos o servidor. Tudo é igual ao usar Express ou Koa.

Criar configuração


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";

Nosso arquivo de configuração. As configurações são transferidas do ambiente de inicialização, mas também adicionaremos valores padrão. A função Deno.env () é um análogo do process.env no Node.js.

Adicionando um modelo de usuário


models / user.ts

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

Criando um arquivo de rota


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;

Mais uma vez, nada de anormal. Criamos um roteador e adicionamos várias rotas a ele. Parece que você copiou o código de um aplicativo Express, certo?

Manipulação de eventos para rotas


manipuladores / getUsers.ts

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

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

Retorna todos os usuários. Se você nunca usou Koa, eu explicarei. O objeto de resposta é análogo ao res no Express. O objeto res no Express possui alguns métodos, como json ou send , que são usados ​​para enviar uma resposta. Em Oak e Koa, precisamos definir o valor que queremos retornar à propriedade response.body .

manipuladores / 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;
};

Tudo é fácil aqui também. O manipulador retorna o usuário com o ID desejado.

manipuladores / 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 manipulador é responsável por criar o usuário.

manipuladores / 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" };
};

O manipulador verifica se o usuário com o ID especificado existe e atualiza os dados do usuário.

manipuladores / 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" };
};

Responsável por excluir um usuário.

Também é aconselhável processar solicitações de rotas inexistentes e retornar uma mensagem de erro.

manipuladores / notFound.ts

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

Adicionando serviços


Antes de criar serviços que funcionem com os dados do usuário, precisamos fazer dois pequenos serviços auxiliares.

services / createId.ts

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

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

Cada novo usuário receberá um ID exclusivo. Usaremos o módulo uuid da biblioteca padrão do Deno para gerar um número aleatório.

services / 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 serviço ajudará a interagir com o arquivo JSON no qual os dados do usuário serão armazenados.
Para obter todos os usuários, leia o conteúdo do arquivo. A função readFile retorna um objeto do tipo Uint8Array , que deve ser convertido em um tipo String antes de entrar em um arquivo JSON .

E, finalmente, o principal serviço para trabalhar com dados do usuário.

services / users.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);
};

Há muito código aqui, mas isso é puro TypeScript.

Erro no processamento


O que poderia ser pior do que aconteceria no caso de um erro de serviço ao trabalhar com dados do usuário? O programa inteiro pode falhar. Para evitar esse cenário, você pode usar a construção try / catch em cada manipulador. Mas existe uma solução mais elegante - adicione middleware na frente de cada rota e evite erros inesperados que possam ocorrer.

middlewares / error.ts

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

Antes de iniciar o próprio programa, precisamos adicionar dados para executar.

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"
  }
]

Isso é tudo! Agora você pode tentar executar nosso aplicativo:

deno -A index.ts

O sinalizador “A” significa que o programa não precisa receber permissões separadas. Não esqueça que o uso desse sinalizador não é seguro na produção.

Provavelmente, você verá muitas linhas com download (Download) e compilação (Compilar). No final, a linha cobiçada deve aparecer:

Listening on 4000

Resumir


Quais ferramentas usamos?

  1. Objeto global Deno para leitura / gravação de arquivos
  2. uuid da biblioteca padrão do Deno para criar um ID exclusivo
  3. oak - uma estrutura de terceiros inspirada no Koa para o Node.js
  4. Objetos Pure Typescript, TextEncode ou JSON incluídos em Javascript

Qual é a diferença do Node.js?


  • Não é necessário instalar e configurar o compilador para o Typescript ou outras ferramentas como o ts-node. Você pode simplesmente executar o programa com o comando deno index.ts
  • Inclusão de todos os módulos de terceiros diretamente no código, sem a necessidade de instalação preliminar
  • Falta de package.json e package-lock.json
  • A ausência de node_modules no diretório raiz do nosso programa. Todos os arquivos baixados estão localizados no cache global

Se necessário, o código fonte pode ser encontrado aqui .

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


All Articles