[JavaScript, Node.JS, MongoDB, TypeScript] Практическое знакомство с Deno: разрабатываем REST API + MongoDB + Linux

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Янв-2021 01:30

Всем привет. В этот раз я решил сделать нечто более интересное, чем очередной бот, поэтому далее я покажу как реализовать REST API с Deno, подключить и использовать MongoDB в качестве базы данных, и всё это запустить из под Linux.Видео версия данной заметки доступна ниже:Извините, данный ресурс не поддреживается. :( Описание задачиВ качестве примера я выбрал Github Gists API и следующие методы:
  • [POST] Create a gist;
  • [GET] List public gists;
  • [GET] Get a gist;
  • [PATCH] Update a gist;
  • [DELETE] Delete a gist.
Создание проектаДля начала мы добавляем файл api/mod.ts :
console.log('hello world');
И проверяем, что всё работает командой deno run mod.ts:
mod.tsДобавление зависимостейСоздаём файл api/deps.ts и добавляем следующие зависимости:
  • Пакет oak для работы с API;
  • Пакет mongo для работы с MongoDB;
/* REST API */
export { Application, Router } from "<https://deno.land/x/oak/mod.ts>";
export type { RouterContext } from "<https://deno.land/x/oak/mod.ts>";
export { getQuery } from "<https://deno.land/x/oak/helpers.ts>";
/* MongoDB driver */
export { MongoClient, Bson } from "<https://deno.land/x/mongo@v0.21.0/mod.ts>";
Отступление: В отличие от NodeJS, авторы Deno отказались от поддержки npm и node_modules, а необходимые библиотеки подключаются по url и кешируются локально. Сами библиотеки можно найти в разделе Third Party Modules на сайте http://deno.land.Добавление API BoilerplateДалее, добавляем код для запуска API в файл mod.ts:
import { Application, Router } from "./deps.ts";
const router = new Router();
router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  });
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8000 });
Причём функции Application и Router импортируем уже из локального файла deps.ts.Проверим, что всё было сделано верно:
  • Запускаем приложение командой deno run --allow-net mod.ts;
  • Открываем в браузере http://localhost:8000;
  • Получаем страницу с сообщением 'Hello world!';

Отступление: Deno позиционируется как secure by default. Другими словами, у запускаемого приложения (скрипта) не будет доступа к сети (--allow-net, файловой системе (--allow-readи --allow-write, параметрам окружения (--allow-env) пока этот доступ явно не разрешён.Добавление метода POST /gistsПришло время добавить первый метод, который будет сохранять запись в базу данных.Прежде всего опишем контракт:
  • [POST] /gists
  • Параметры:
    • content: string | body;
  • Ответы:
    • 201 Created;
    • 400 Bad Request;
ОбработчикДобавляем папку handlers и файл create.ts, в котором будет расположен handler (обработчик) запроса:
import { RouterContext } from "../deps.ts";
import { createGist } from "../service.ts";
export async function create(context: RouterContext) {
  if (!context.request.hasBody) {
    context.throw(400, "Bad Request: body is missing");
  }
  const body = context.request.body();
  const { content } = await body.value;
  if (!content) {
    context.throw(400, "Bad Request: content is missing");
  }
  const gist = await createGist(content);
  context.response.body = gist;
  context.response.status = 201;
}
В этой функции мы:
  • Валидируем входные значения (request.hasBody и !content);
  • Вызываем функцию createGist нашего сервиса (добавим далее);
  • Возвращаем добавленный объект в ответе и 201 Created.
СервисДалее, нам необходимо передать управление из обработчика в сервис (добавляем service.ts):
import { insertGist } from "./db.ts";
export async function createGist(content: string): Promise<IGist> {
  const values = {
    content,
    created_at: new Date(),
  };
  const _id = await insertGist(values);
  return {
    _id,
    ...values,
  };
}
interface IGist {
  _id: string;
  content: string;
  created_at: Date;
}
В данном случае функция принимает единственный аргумент content: string и возвращает объект, структура которого описывается интерфейсом IGist.РепозиторийПоследним этапом обработки запроса является сохранение записи в MongoDB. Для этого мы добавляем файл db.ts и соответствующую функцию:
import { Collection } from "<https://deno.land/x/mongo@v0.21.0/src/collection/collection.ts>";
import { Bson, MongoClient } from "./deps.ts";
async function connect(): Promise<Collection<IGistSchema>> {
  const client = new MongoClient();
  await client.connect("mongodb://localhost:27017");
  return client.database("gist_api").collection<IGistSchema>("gists");
}
export async function insertGist(gist: any): Promise<string> {
  const collection = await connect();
  return (await collection.insertOne(gist)).toString();
}
interface IGistSchema {
  _id: { $oid: string };
  content: string;
  created_at: Date;
}
В этом файле мы:
  • Импортируем необходимые типы и функции для работы с MongoDB;
  • Подключаемся к базе данных gist_api в функции connect;
  • Описываем формат объектов, которые хранятся в коллекции gist_api интерфейсом IGistSchema;
  • Сохраняем объект методом insertOne и возвращаем его идентификатор (inserted id);
Запускаем экземпляр MongoDBДалее мы запускаем терминал, запускаем и проверяем статус нашей базы данных следующими командами:
sudo systemctl start mongod
sudo systemctl status mongod
Если всё было сделано верно, то получим следующий результат:
Отступление: Как установить MongoDB на UbuntuЗапускаем приложение
  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;
  • Открываем Postman и вызываем метод нашего API:
Если всё сделано верно, то в качестве ответа получаем 201 Created и сохранённый объект с проставленным _id:
Отступление: Как вы могли заметить, в процессе разработки мы используем TypeScript без транспайлеров. Причина проста - Deno поддерживает TypeScript из коробки.Добавление метода GET /gistsСледующим методом мы сможем получить записи из базы данных, а заодно реализовать базовую пагинацию.Прежде всего опишем контракт:
  • [GET] /gists
  • Параметры:
    • skip: string | query;
    • limit: string | query;
  • Ответы:
    • 200 OK;
ОбработчикДобавляем файл handlers/list.ts, в котором будет расположен handler (обработчик) запроса:
import { getQuery, RouterContext } from "../deps.ts";
import { getGists } from "../service.ts";
export async function list(context: RouterContext) {
  const { skip, limit } = getQuery(context);
  const gists = await getGists(+skip || 0, +limit || 0);
  context.response.body = gists;
  context.response.status = 200;
}
В этой функции мы:
  • Получаем параметры с query string с помощь функции getQuery;
  • Вызываем функцию getGists нашего сервиса (добавим далее);
  • Возвращаем массив найденных объектов в ответе и 200 OK;
Отступление: Функция сервиса будет принимать аргументы типа number, в то время как в обработчик к нам приходят параметры типа string. Для этого мы делаем приведение типов следующей конструкцией +skip || 0 (корректные значения конвертируются, некорректные приводятся к NaN и игнорируются в пользу 0).СервисДалее, передаём управление из обработчика в сервис:
export function getGists(skip: number, limit: number): Promise<IGist[]> {
  return fetchGists(skip, limit);
}
В данном случае функция принимает аргументы skip: number и limit: number, и возвращает массив объектов, структура которых описывается интерфейсом IGist.РепозиторийПоследним этапом обработки запроса является получение записей из MongoDB. Для этого мы добавляем функцию fetchGists в файл db.ts:
export async function fetchGists(skip: number, limit: number): Promise<any> {
  const collection = await connect();
  return await collection.find().skip(skip).limit(limit).toArray();
}
В этой функции мы:
  • Подключаемся к базе данных gist_api в функции connect;
  • Получаем все записи коллекции, пропускаем skip из них и возвращаем в кол-ве limit;
Запускаем приложение
  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;
  • Открываем Postman и вызываем метод нашего API:
Если всё сделано верно, то в качестве ответа получаем 200 OK и массив ранее добавленных объектов:
Добавление метода GET /gists/:idСледующим методом мы получаем запись из базы данных по её идентификатору.Прежде всего опишем контракт:
  • [GET] /gists/:id
  • Параметры:
    • id: string | path
  • Ответы:
    • 200 OK;
    • 400 Bad Request;
    • 404 Not Found.
ОбработчикДобавляем файл handlers/get.ts, в котором будет расположен handler (обработчик) запроса:
import { RouterContext } from "../deps.ts"
import { getGist } from "../service.ts";
export async function get(context: RouterContext) {
    const { id } = context.params;
    if(!id) {
        context.throw(400, "Bad Request: id is missing");
    }
    const gist = await getGist(id);
    if(!gist) {
        context.throw(404, "Not Found: the gist is missing");
    }
    context.response.body = gist;
    context.response.status = 200;
}
В этой функции мы:
  • Проверяем наличие id и возвращаем 400 если он отсутствует;
  • Запрашиваем объект в базе данных функцией getGist и возвращаем 404 если он не найден (добавим далее);
  • Возвращаем найденный объект и 200 OK;
СервисДалее, передаём управление из обработчика в сервис:
export function getGist(id: string): Promise<IGist> {
    return fetchGist(id);
}
interface IGist {
  _id: string;
  content: string;
  created_at: Date;
}
В данном случае функция принимает аргумент id: string и возвращает объект, структура которого описывается интерфейсом IGist.РепозиторийПоследним этапом обработки запроса является получение записи из MongoDB. Для этого мы добавляем функцию fetchGist в файл db.ts:
export async function fetchGist(id: string): Promise<any> {
  const collection = await connect();
  return await collection.findOne({ _id: new Bson.ObjectId(id) });
}
В этой функции мы:
  • Подключаемся к базе данных gist_api в функции connect;
  • Используем метод findOne для поиска записи удовлетворяющей фильтру по _id;
Запускаем приложение
  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;
  • Открываем Postman и вызываем метод нашего API:
Если всё сделано верно, то в качестве ответа получаем 200 OK и ранее добавленный объект:
Добавление метода PATCH /gists/:idСледующим методом мы обновляем запись в базе данных по её идентификатору.Как и прежде, начинаем с контракта:
  • [PATCH] /gists/:id
  • Параметры:
    • id: string | path
    • content: string | body
  • Ответы:
    • 200 OK;
    • 400 Bad Request;
    • 404 Not Found.
ОбработчикДобавляем файл handlers/update.ts, в котором будет расположен handler (обработчик) запроса:
import { RouterContext } from "../deps.ts";
import { getGist, patchGist } from "../service.ts";
export async function update(context: RouterContext) {
  const { id } = context.params;
  if (!id) {
    context.throw(400, "Bad Request: id is missing");
  }
  const body = context.request.body();
  const { content } = await body.value;
  if (!content) {
    context.throw(400, "Bad Request: content is missing");
  }
  const gist = await getGist(id);
  if (!gist) {
    context.throw(404, "Not Found: the gist is missing");
  }
  await patchGist(id, content);
  context.response.status = 200;
}
В этой функции мы:
  • По аналогии проверяем наличие id и возвращаем 400 если он отсутствует;
  • Валидируем входное значение !content;
  • Запрашиваем объект в базе данных функцией getGist и возвращаем 404 если он не найден;
  • Обновляем объект в базе данных функцией patchGist (добавим далее);
  • Возвращаем 200 OK.
СервисДалее, передаём управление из обработчика в сервис:
export async function patchGist(id: string, content: string): Promise<any> {
  return updateGist({ id, content });
}
interface IGist {
  _id: string;
  content: string;
  created_at: Date;
}
В данном случае функция принимает аргументы id: string и content: string, и возвращает any.РепозиторийПоследним этапом обработки запроса является обновлении записи в MongoDB. Для этого мы добавляем функцию updateGist в файл db.ts:
export async function updateGist(gist: any): Promise<any> {
  const collection = await connect();
  const filter = { _id: new Bson.ObjectId(gist.id) };
  const update = { $set: { content: gist.content } };
  return await collection.updateOne(filter, update);
}
В этой функции мы:
  • Подключаемся к базе данных gist_api в функции connect;
  • Описываем фильтр filter объектов, которые мы хотим обновить;
  • Описываем инструкцию update, которую применяем для обновления найденных объектов;
  • Используем метод updateOne собрав всё воедино;
Запускаем приложение
  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;
  • Открываем Postman и вызываем метод нашего API:
Если всё сделано верно, то в качестве ответа получаем 200 OK:
Добавление метода DELETE /gists/:idПоследним по списку, но не по важности, мы добавляем метод удаления записей из базы данных по идентификатору.По традиции, начинаем с контракта:
  • [DELETE] /gists/:id
  • Параметры:
    • id: string | path
  • Ответы:
    • 204 No Content;
    • 404 Not Found.
ОбработчикДобавляем файл handlers/remove.ts, в котором будет расположен handler (обработчик) запроса:
import { RouterContext } from "../deps.ts";
import { getGist, removeGist } from "../service.ts";
export async function remove(context: RouterContext) {
  const { id } = context.params;
  if (!id) {
    context.throw(400, "Bad Request: id is missing");
  }
  const gist = await getGist(id);
  if (!gist) {
    context.throw(404, "Not Found: the gist is missing");
  }
  await removeGist(id);
  context.response.status = 204;
}
В этой функции мы:
  • По аналогии проверяем наличие id и возвращаем 400 если он отсутствует;
  • Запрашиваем объект в базе данных функцией getGist и возвращаем 404 если он не найден;
  • Удаляем объект из базы данных функцией removeGist (добавим далее);
  • Возвращаем 204 No Content.
СервисДалее, передаём управление из обработчика в сервис:
export function removeGist(id: string): Promise<number> {
  return deleteGist(id);
}
В данном случае функция принимает единственный аргумент id: string и возвращает number.РепозиторийПоследним этапом обработки запроса является удаление записи из коллекции MongoDB. Для этого мы добавляем функцию deleteGist в файл db.ts:
export async function deleteGist(id: string): Promise<any> {
  const collection = await connect();
  return await collection.deleteOne({ _id: new Bson.ObjectId(id) });
}
В этой функции мы:
  • Подключаемся к базе данных gist_api в функции connect;
  • Используем метод deleteOne для удаления объекта удовлетворяющего фильтру по _id;
Запускаем приложение
  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;
  • Открываем Postman и вызываем метод нашего API:
Если всё сделано верно, то в качестве ответа получаем 204 No Content:
Отступление: В данном случае фактическое удаление объекта из коллекции выбрано для наглядности. В реальных приложениях я предпочитаю добавить и обновлять у объекта поле isDeleted: boolean.FAQВызывая методы API я всегда получаю только 404 Not FoundУбедитесь что вы не забыли сконфигурировать router в файле mod.ts соответствующими обработчиками:
import { Application, Router } from "./deps.ts";
import { list } from "./handlers/list.ts";
import { create } from "./handlers/create.ts";
import { remove } from "./handlers/remove.ts";
import { get } from "./handlers/get.ts";
import { update } from "./handlers/update.ts";
const app = new Application();
const router = new Router();
router
  .post("/gists", create)
  .get("/gists", list)
  .get("/gists/:id", get)
  .delete("/gists/:id", remove)
  .patch("/gists/:id", update);
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8000 });
Вызывая методы API я получаю 500 Internal Server ErrorОтловить ошибку можно следующим способом:
const app = new Application();
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    console.log(err);
  }
});
...
Ссылки ЗаключениеСпасибо за то что дочитали до конца.
В заключении упомяну, что к сожалению, не каждое из моих видео удаётся опубликовать в текстовом виде, поэтому если данные тема и формат вам интересны, то я приглашаю вас подписаться на телеграм-канал.

===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_node.js, #_mongodb, #_typescript, #_deno, #_rest_api, #_mongodb, #_typescript, #_linux, #_ubunty, #_tutorial, #_javascript, #_node.js, #_mongodb, #_typescript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 23:05
Часовой пояс: UTC + 5