Всем привет. В этот раз я решил сделать нечто более интересное, чем очередной бот, поэтому далее я покажу как реализовать 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 mongodsudo 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 аналог которой мы разрабатываем;
Заключение
Спасибо за то что дочитали до конца.
В заключении упомяну, что к сожалению, не каждое из моих видео удаётся опубликовать в текстовом виде, поэтому если данные тема и формат вам интересны, то я приглашаю вас подписаться на телеграм-канал.