Создание небольшого API на Deno

от автора

В этом посте я хотел бы рассказать и показать процесс создания небольшого API с помощью Deno. Deno — новейшая среда для запуска Javascript и Typescript, разработанная создателем Node.js — Райаном Далем.



Наши цели:

  • Разработать API, которое будет работать с данными пользователей
  • Предоставить возможность использовать методы GET, POST, PUT и DELETE
  • Сохранять и обновлять данные пользователей в локальный JSON файл
  • Воспользоваться фреймворком для ускорения разработки

Единственное, что нам потребуется установить — Deno. Deno поддерживает Typescript прямо из коробки. Для этого примера я использовал Deno версии 0.22 и этот код может не заработать на дальнейших версиях.
Версию установленного Deno можно узнать командой deno version в терминале.

Структура программы

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

Как вы могли заметить, выглядит это как небольшое web приложение на Node.js.

  • handlers — содержит обработчики маршрутов
  • middlewares — содержит в себе функции, которые будут запущены при каждом запросе
  • models — содержит в себе обозначение моделей, в нашем случае это только интерфейс пользователя
  • services — содержит… сервисы!
  • config.ts — конфигурационный файл приложения
  • index.ts — входная точка для нашего приложения
  • routing.ts — содержит маршруты API

Выбор фреймворка

Существует много великолепных фреймворков для Node.js. Один из самых популярных — Express. Также есть современная версия Express’a — Koa. К сожалению, Deno не поддерживает библиотеки Node.js и выбор гораздо меньше, но существует фреймворк для Deno, который создан на основе Koa — Oak. Мы воспользуемся им для нашего небольшого проекта. Если вы никогда не использовали Koa, не переживайте, он многим похож на Express.

Создание входной точки приложения

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

В первой строчке мы используем одну из главных фишек Deno — импортирование модулей прямо из интернета. После этого нет ничего необычного: создаем приложение, добавляем middleware, маршруты и, наконец, запускаем сервер. Все также, как и в случае использования Express или Koa.

Создание конфигурации

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

Наш файл конфигурации. Настройки переносятся из среды запуска, но мы также добавим дефолтные значение. Функция Deno.env() является аналогом process.env в Node.js.

Добавление модели пользователя

models/user.ts

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

Создание файла с маршрутами

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; 

Опять же, ничего необычного. Мы создали маршрутизатор и добавили несколько маршрутов в него. Выглядит практически так, как будто вы скопировали код из Express приложения, не так ли?

Обработка событий для маршрутов

handlers/getUsers.ts

import { getUsers } from "../services/users.ts";  export default async ({ response }) => {   response.body = await getUsers(); }; 

Возвращает всех пользователей. Если вы никогда не использовали Koa, то разъясню. Объект response является аналогом res в Express. Объект res в Express имеет пару методов вроде json или send, которые служат для отправки ответа. В Oak и Koa нам нужно установить значение, которое мы хотим вернуть в свойство 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; }; 

Тут тоже все легко. Обработчик возвращает пользователя с нужным Id.

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

Этот обработчик отвечает за создание пользователя.

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

Обработчик проверяет существует ли пользователь с указанным ID и обновляет данные пользователей.

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

Отвечает за удаление пользователя.

Также желательно обрабатывать запросы на несуществующие маршруты и возвращать сообщение с ошибкой.

handlers/notFound.ts

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

Добавление сервисов

Перед созданием сервисов, которые будут работать с данными пользователей, нам нужно сделать два небольших вспомогательных сервиса.

services/createId.ts

import { v4 as uuid } from "https://deno.land/std/uuid/mod.ts";  export default () => uuid.generate(); 

Каждый новый пользователь будет получать уникальный id. Воспользуемся модулем uuid из стандартной библиотеки Deno для генерации случайного числа.

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

Этот сервис будет помогать взаимодействовать с JSON файлом, в котором будут храниться данные пользователей.
Чтобы получить всех пользователей, прочитаем содержимого файла. Функция readFile возвращает объект типа Uint8Array, которые перед тем как занести в JSON файл, нужно преобразовать в тип String.

На и наконец, основной сервис для работы с данными пользователей.

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

Здесь много кода, но это чистый Typescript.

Обработка ошибок

Что может быть хуже, чем то, что случится в случае ошибки сервиса, работающего с данными пользователей? Вся программа может крашнуться. Для избежания подобного сценария, можно воспользоваться конструкцией try/catch в каждом обработчике. Но существует более изящное решение — добавим middleware перед каждым маршрутом и предотвратим все неожиданные ошибки, которые могут возникнуть.

middlewares/error.ts

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

Перед тем, как запустить саму программу, нам нужно добавить данные для запуска.

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

Вот и все! Теперь можно попробовать запустить наше приложение:

deno -A index.ts

Флаг “А” обозначает, что программе не нужно давать каких-либо отдельных разрешений. Не забывайте, что использовать этот флаг небезопасно в продакшене.

Скорее всего, вы увидите много строчек с загрузкой (Download) и компиляцией (Compile). В конце концов должна появиться заветная строчка:

Listening on 4000

Подведем итоги

Какими инструментами мы воспользовались?

  1. Глобальный объект Deno для чтения/записи файлов
  2. uuid из стандартной библиотеки Deno для создание уникального id
  3. oak — third-party фреймворк, вдохновленный Koa для Node.js
  4. Чистый Typescript, объекты TextEncode или JSON, входящие в Javascript

В чем же отличие от Node.js?

  • Отсутствие необходимости в установке и настройки компилятора для Typescript или других инструментов вроде ts-node. Можно просто запустить программу командой deno index.ts
  • Включение всех сторонних модулей прямо в код без необходимости предварительной установки
  • Отсутствие package.json и package-lock.json
  • Отсутствие node_modules в коренной директории нашей программы. Все загруженные файлы расположены в глобальном кэше

При необходимости, исходный код вы можете найти по ссылке.

ссылка на оригинал статьи https://habr.com/ru/post/487806/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *