Création d'une petite API Deno

Dans cet article, je voudrais expliquer et montrer le processus de création d'une petite API à l'aide de Deno. Deno est le dernier lanceur Javascript et Typescript développé par le créateur de Node.js Ryan Dahl.



Nos objectifs :

  • Développer une API qui fonctionnera avec les données utilisateur
  • Fournir la possibilité d'utiliser les méthodes GET, POST, PUT et DELETE
  • Enregistrer et mettre à jour les données utilisateur dans un fichier JSON local
  • Utilisez le cadre pour accélérer le développement

La seule chose que nous devons installer est Deno. Deno prend en charge Typescript dès la sortie de la boîte. Pour cet exemple, j'ai utilisé Deno version 0.22 et ce code pourrait ne pas fonctionner sur les futures versions.
La version du Deno installé peut être trouvée avec la commande deno version dans le terminal.

Structure du programme


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

Comme vous pouvez le voir, cela ressemble à une petite application Web sur Node.js.

  • handlers - contient les gestionnaires d'itinéraires
  • middlewares - contient des fonctions qui seront lancées à chaque demande
  • modèles - contient la désignation des modèles, dans notre cas ce n'est qu'une interface utilisateur
  • services - contient ... des services!
  • config.ts - fichier de configuration d'application
  • index.ts est le point d'entrée de notre application
  • routing.ts - contient les routes API

Sélection du cadre


Il existe de nombreux grands frameworks pour Node.js. L'un des plus populaires est Express . Il existe également une version moderne d'Express'a - Koa . Malheureusement, Deno ne prend pas en charge les bibliothèques Node.js et le choix est beaucoup plus restreint, mais il existe un cadre pour Deno, basé sur Koa- Oak . Nous l'utiliserons pour notre petit projet. Si vous n'avez jamais utilisé Koa, ne vous inquiétez pas, cela ressemble beaucoup à Express.

Création d'un point d'entrée d'application


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

Dans la première ligne, nous utilisons l'une des principales puces Deno - importer des modules directement à partir d'Internet. Après cela, il n'y a rien d'inhabituel: nous créons une application, ajoutons un middleware, des routes et, enfin, démarrons le serveur. Tout est le même que lorsque vous utilisez Express ou Koa.

Créer une configuration


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

Notre fichier de configuration. Les paramètres sont transférés depuis l'environnement de lancement, mais nous ajouterons également des valeurs par défaut. La fonction Deno.env () est un analogue de process.env dans Node.js.

Ajout d'un modèle utilisateur


models / user.ts

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

Création d'un fichier d'itinéraire


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;

Encore une fois, rien d'inhabituel. Nous avons créé un routeur et y avons ajouté plusieurs routes. Il semble que vous ayez copié du code à partir d'une application Express, non?

Gestion des événements pour les itinéraires


handlers / getUsers.ts

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

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

Renvoie tous les utilisateurs. Si vous n'avez jamais utilisé Koa, je vais vous expliquer. L'objet de réponse est analogue à res dans Express. L'objet res dans Express a quelques méthodes comme json ou send , qui sont utilisées pour envoyer une réponse. Dans Oak et Koa, nous devons définir la valeur que nous voulons renvoyer à la propriété response.body .

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

Ici aussi, tout est facile. Le gestionnaire renvoie l'utilisateur avec l'ID souhaité.

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

Ce gestionnaire est responsable de la création de l'utilisateur.

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

Le gestionnaire vérifie si l'utilisateur avec l'ID spécifié existe et met à jour les données utilisateur.

handlers / 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 la suppression d'un utilisateur.

Il est également conseillé de traiter les demandes d'itinéraires inexistants et de renvoyer un message d'erreur.

handlers / notFound.ts

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

Ajout de services


Avant de créer des services qui fonctionneront avec les données des utilisateurs, nous devons créer deux petits services auxiliaires.

services / createId.ts

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

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

Chaque nouvel utilisateur recevra un identifiant unique. Nous utiliserons le module uuid de la bibliothèque standard Deno pour générer un nombre aléatoire.

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

Ce service aidera à interagir avec le fichier JSON dans lequel les données utilisateur seront stockées.
Pour obtenir tous les utilisateurs, lisez le contenu du fichier. La fonction readFile renvoie un objet de type Uint8Array , qui doit être converti en type String avant d' entrer dans un fichier JSON .

Et enfin, le service principal pour travailler avec les données des utilisateurs.

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

Il y a beaucoup de code ici, mais c'est du pur Script.

Erreur de traitement


Qu'est-ce qui pourrait être pire que ce qui se passerait en cas d'erreur de service lors de l'utilisation des données utilisateur? L'ensemble du programme peut se bloquer. Pour éviter ce scénario, vous pouvez utiliser la construction try / catch dans chaque gestionnaire. Mais il existe une solution plus élégante: ajoutez un middleware devant chaque route et évitez les erreurs inattendues qui pourraient survenir.

middlewares / error.ts

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

Avant de démarrer le programme lui-même, nous devons ajouter des données à exécuter.

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

C'est tout! Vous pouvez maintenant essayer d'exécuter notre application:

deno -A index.ts

le drapeau «A» signifie que le programme n'a pas besoin de se voir accorder d'autorisations distinctes. N'oubliez pas que l'utilisation de ce drapeau n'est pas sûre en production.

Très probablement, vous verrez de nombreuses lignes avec téléchargement (téléchargement) et compilation (compilation). Au final, la ligne convoitée devrait apparaître:

Listening on 4000

Résumer


Quels outils avons-nous utilisés?

  1. Objet global Deno pour lire / écrire des fichiers
  2. uuid à partir de la bibliothèque standard Deno pour créer un identifiant unique
  3. oak - un framework tiers inspiré de Koa pour Node.js
  4. Objets Typescript, TextEncode ou JSON purs inclus dans Javascript

Quelle est la différence avec Node.js?


  • Pas besoin d'installer et de configurer le compilateur pour Typescript ou d'autres outils comme ts-node. Vous pouvez simplement exécuter le programme avec la commande deno index.ts
  • Inclusion de tous les modules tiers directement dans le code sans nécessiter d'installation préalable
  • Manque de package.json et package-lock.json
  • L'absence de node_modules dans le répertoire racine de notre programme. Tous les fichiers téléchargés se trouvent dans le cache global

Si nécessaire, le code source peut être trouvé ici .

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


All Articles