Всем привет. В этот раз я решил сделать нечто более интересное, чем очередной бот, поэтому далее я покажу как реализовать 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:
Добавление зависимостей
Создаём файл api/deps.ts и добавляем следующие зависимости:
/* 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); } }); ...
Ссылки
-
GitHub Gist API аналог которой мы разрабатываем;
Заключение
Спасибо за то что дочитали до конца.
В заключении упомяну, что к сожалению, не каждое из моих видео удаётся опубликовать в текстовом виде, поэтому если данные тема и формат вам интересны, то я приглашаю вас подписаться на телеграм-канал.
ссылка на оригинал статьи https://habr.com/ru/post/539212/
Добавить комментарий