- Разработать 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
Подведем итоги
Какими инструментами мы воспользовались?
- Глобальный объект Deno для чтения/записи файлов
- uuid из стандартной библиотеки Deno для создание уникального id
- oak — third-party фреймворк, вдохновленный Koa для Node.js
- Чистый 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/

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