Erstellen einer kleinen Deno-API

In diesem Beitrag möchte ich den Prozess der Erstellung einer kleinen API mit Deno erläutern und zeigen. Deno ist der neueste Javascript- und Typescript-Launcher, der von Node.js Erfinder Ryan Dahl entwickelt wurde.



Unsere Ziele :

  • Entwickeln Sie eine API, die mit Benutzerdaten arbeitet
  • Bieten Sie die Möglichkeit, die Methoden GET, POST, PUT und DELETE zu verwenden
  • Speichern und aktualisieren Sie Benutzerdaten in der lokalen JSON-Datei
  • Verwenden Sie das Framework, um die Entwicklung zu beschleunigen

Das einzige, was wir installieren müssen, ist Deno. Deno unterstützt Typescript sofort. In diesem Beispiel habe ich Deno Version 0.22 verwendet und dieser Code funktioniert möglicherweise nicht in zukünftigen Versionen.
Die Version des installierten Deno kann mit dem Befehl deno version im Terminal ermittelt werden.

Programmstruktur


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

Wie Sie sehen können, sieht es aus wie eine kleine Webanwendung auf Node.js.

  • Handler - Enthält Routenhandler
  • Middleware - enthält Funktionen, die bei jeder Anforderung gestartet werden
  • Modelle - enthält die Bezeichnung von Modellen, in unserem Fall ist es nur eine Benutzeroberfläche
  • Dienstleistungen - enthält ... Dienstleistungen!
  • config.ts - Anwendungskonfigurationsdatei
  • index.ts ist der Einstiegspunkt für unsere Anwendung
  • routing.ts - enthält API-Routen

Rahmenauswahl


Es gibt viele großartige Frameworks für Node.js. Eines der beliebtesten ist Express . Es gibt auch eine moderne Version von Express'a - Koa . Leider unterstützt Deno keine Node.js-Bibliotheken und die Auswahl ist viel kleiner, aber es gibt ein Framework für Deno, das auf Koa- Oak basiert . Wir werden es für unser kleines Projekt verwenden. Wenn Sie Koa noch nie benutzt haben, machen Sie sich keine Sorgen, es sieht Express sehr ähnlich.

Erstellen eines Anwendungseinstiegspunkts


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

In der ersten Zeile verwenden wir einen der wichtigsten Deno-Chips - den Import von Modulen direkt aus dem Internet. Danach gibt es nichts Ungewöhnliches: Wir erstellen eine Anwendung, fügen Middleware und Routen hinzu und starten schließlich den Server. Alles ist wie bei Express oder Koa.

Konfiguration erstellen


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

Unsere Konfigurationsdatei. Die Einstellungen werden aus der Startumgebung übertragen, es werden jedoch auch Standardwerte hinzugefügt. Die Deno.env () -Funktion ist ein Analogon zu process.env in Node.js.

Hinzufügen eines Benutzermodells


models / user.ts

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

Erstellen einer Routendatei


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;

Wieder nichts Ungewöhnliches. Wir haben einen Router erstellt und ihm mehrere Routen hinzugefügt. Es sieht fast so aus, als hätten Sie Code aus einer Express-Anwendung kopiert, oder?

Ereignisbehandlung für Routen


handlers / getUsers.ts

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

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

Gibt alle Benutzer zurück. Wenn Sie Koa noch nie benutzt haben, erkläre ich es Ihnen. Das Antwortobjekt ist analog zu res in Express. Das res- Objekt in Express verfügt über einige Methoden wie json oder send , mit denen eine Antwort gesendet wird. In Oak und Koa müssen wir den Wert festlegen, den wir an die Eigenschaft response.body zurückgeben möchten .

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

Auch hier ist alles einfach. Der Handler gibt den Benutzer mit der gewünschten ID zurück.

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

Dieser Handler ist für die Erstellung des Benutzers verantwortlich.

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

Der Handler prüft, ob der Benutzer mit der angegebenen ID vorhanden ist, und aktualisiert die Benutzerdaten.

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

Verantwortlich für das Löschen eines Benutzers.

Es ist auch ratsam, Anforderungen für nicht vorhandene Routen zu verarbeiten und eine Fehlermeldung zurückzugeben.

Handler / notFound.ts

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

Hinzufügen von Diensten


Bevor wir Dienste erstellen, die mit Benutzerdaten arbeiten, müssen wir zwei kleine Hilfsdienste erstellen.

services / createId.ts

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

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

Jeder neue Benutzer erhält eine eindeutige ID. Wir werden das uuid-Modul aus der Deno-Standardbibliothek verwenden, um eine Zufallszahl zu generieren.

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

Dieser Dienst hilft bei der Interaktion mit der JSON-Datei, in der Benutzerdaten gespeichert werden.
Lesen Sie den Inhalt der Datei, um alle Benutzer zu erhalten. Die Funktion readFile gibt ein Objekt vom Typ Uint8Array zurück , das vor der Eingabe in eine JSON-Datei in einen String- Typ konvertiert werden muss .

Und schließlich der Hauptdienst für die Arbeit mit Benutzerdaten.

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

Hier gibt es viel Code, aber es ist reines Typoskript.

Fehlerverarbeitung


Was könnte schlimmer sein als das, was im Falle eines Servicefehlers beim Arbeiten mit Benutzerdaten passieren würde? Das gesamte Programm kann abstürzen. Um dieses Szenario zu vermeiden, können Sie das try / catch-Konstrukt in jedem Handler verwenden. Es gibt jedoch eine elegantere Lösung: Fügen Sie vor jeder Route Middleware hinzu und vermeiden Sie unerwartete Fehler.

Middleware / error.ts

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

Bevor wir das Programm selbst starten, müssen wir Daten hinzufügen, um es auszuführen.

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

Das ist alles! Jetzt können Sie versuchen, unsere Anwendung auszuführen:

deno -A index.ts

Das Flag "A" bedeutet, dass dem Programm keine separaten Berechtigungen erteilt werden müssen. Vergessen Sie nicht, dass die Verwendung dieses Flags in der Produktion unsicher ist.

Höchstwahrscheinlich werden Sie viele Zeilen mit Download (Download) und Kompilierung (Kompilieren) sehen. Am Ende sollte die begehrte Linie erscheinen:

Listening on 4000

Zusammenfassen


Welche Tools haben wir verwendet?

  1. Deno globales Objekt zum Lesen / Schreiben von Dateien
  2. uuid aus der Deno-Standardbibliothek, um eine eindeutige ID zu erstellen
  3. Eiche - ein von Koa für Node.js inspiriertes Framework von Drittanbietern
  4. In Javascript enthaltene reine Typescript-, TextEncode- oder JSON-Objekte

Was ist der Unterschied zu Node.js?


  • Der Compiler muss nicht für Typescript oder andere Tools wie ts-node installiert und konfiguriert werden. Sie können das Programm einfach mit dem Befehl deno index.ts ausführen
  • Alle Module von Drittanbietern direkt in den Code aufnehmen, ohne dass eine Vorinstallation erforderlich ist
  • Fehlen von package.json und package-lock.json
  • Das Fehlen von node_modules im Stammverzeichnis unseres Programms. Alle heruntergeladenen Dateien befinden sich im globalen Cache

Den Quellcode finden Sie ggf. hier .

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


All Articles