CRUD API на Deno и PostgreSQL: работаем с динозавром

    Всем привет. В преддверии старта курса «Fullstack разработчик JavaScript», хотим поделиться интересным материалом, который прислал наш внештатный автор. Данная статья не имеет отношения к программе курса, но наверняка будет интересна, как небольшой обзор на Deno.





    "-Райан, мы тут концепцию CLI команд для Deno продумываем. Сколько флагов для secure runtime добавить?"
    "-Да"
    "-Райан, нам надо придумать символ языка, чтобы все понимали, что мы делаем что-то прям новое. Какое животное выберем?"
    "-Динозавра"

    Всем привет. Deno становится все известнее и популярнее, выходит огромное количество новых видео и текстовых гайдов на эту тему, но преимущественно на английском языке. На Хабре тоже выходило несколько статей на тему Deno, к примеру здесь и здесь, но преимущественно статьи рассказывают о Deno в теоретическом плане. Сегодня мы попробуем создать небольшое CRUD Api, которое может обслужить, к примеру, todo-приложение на frontend, и я поделюсь с вам своими впечатлениями от этого динозавра в реальном кодинге.

    Теоретическое вступление




    Для тех кто в танке, я расскажу, что такое Deno, если вы не читали, например, какую-то из статей выше. Deno — это среда выполнения runtime, работающая на JavaScript и TypeScript (поддержка TypeScript осуществляется из коробки, версия компилятора TypeScript жестко зашита в версию Deno, сменить ее получится только если смените версию Deno), которая базируется на движке V8 и языке программирования Rust. Deno был создан Райаном Далом, создателем Node.js, и главные качества Deno — это производительность и безопасность. О появлении Deno на свет было объявлено в 2018, и о чем многие источники забывают упомянуть — испытал большое влияние Golang (да и вообще изначально был написан на Golang, однако впоследствии пришлось переписать на Rust).

    Стандартная библиотека была создана по образцу стандартной библиотеки Go, да и множество инструментов или их реализации перешли из Golang в Deno, к примеру, отсутствие экосистемы вроде npm и подкачивание библиотек напрямую из веб-ресурсов на этапе первоначальной сборки (что вызывает некий ступор у Node.js разработчиков, которые вполне естественно не были знакомы с подобной системой у Go). Так для кого Deno сейчас? Если вам нравятся типизируемые языки, нравятся идеи и инструменты Golang, но хочется писать бэк на чем-то более простом — пора попробовать Deno в реальности. Однако, если вы хотите подождать, пока платформа «дозреет», появится большее количество библиотек и ответов на StackOverflow, возможно, вам стоит немного подождать.

    Начинаем создавать


    Несмотря на молодость Deno, вокруг него успела сформироваться какая-никакая экосистема (которую конечно же пока нельзя сравнивать с экосистемой Node.js), которая позволяет вам покрыть базовые потребности в создании своего API. Давайте определимся, что создаем: я хочу создать простое CRUD Api, используя Deno и TypeScript на бекенде (раз есть поддержка TypeScript, то почему бы ей не воспользоваться), PostgreSQL в качестве базы данных (вообще я хотел использовать MySQL, но случайно сломал себе доступ по localhost и не смог починить. Любопытный факт, что по внешнему ip встроенный сервер PHP 7.4 смог соединиться с моей базой MySQL, а Deno показывал packets out of order), и запросы у нас будут выглядеть следующим образом:

    Метод Routes Результат выполнения
    GET /api/todos/get Получение всех todo
    POST /api/todos/post Создание нового дела
    PATCH /api/todos/:id Обновление определенного дела
    DELETE /api/todos/:id Удаление дела по id


    Отлично, со схемой запросов мы определились, сервер PostgreSQL запустили (если, что скачать его можно отсюда, весьма приятный GUI-клиент Postico), базу данных crud_api мы создали.
    Что дальше? Руками с помощью sql-запросов или клиента таблицу создавать конечно можно, но хочется посмотреть, как там с миграциями у Deno. На этот случай нам пригодиться библиотека nessia, позволяющая создавать миграции похожие на миграции в Laravel. Но перед тем, как мы займемся миграциями, давайте поговорим об организации среды разработки.

    Буквально на днях вышел официальный плагин для разработки на Deno от JetBrains. Найти его можно здесь. На Visual Studio вам нужно установить плагин Deno, и в папке проекта создать папку настроек .vscode, где в settings.json вы включите поддержку deno (но у меня вроде поддержка работала и без этого):

        "deno.enable":true
    

    На этом с подготовительным этапом все, можем возвращаться к миграциям. Для начала нам нужно ввести команду инициализации, взяв ее из документации библиотеки:

    deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts init
    

    Создаем конфигурирующий файл для нашей библиотеки, с помощью которой мы сможем определить, с какой СУБД и с какой базой данных вы собираетесь произвести коннект.
    Изначально в файле создаются три шаблона — для PostgreSQL, MySQL и Sqlite(по умолчанию экспортятся настройки для PostgreSQL).

    В итоге наш файл nessie.config.ts будет выглядеть следующим образом:

    import { ClientPostgreSQL} from "https://deno.land/x/nessie/mod.ts"; 
    
    const migrationFolder = "./migrations";
    
    const configPg = {
      client: new ClientPostgreSQL(migrationFolder, {
        database: "deno_crud",
        hostname: "localhost",
        port: 5432,
        user: "isakura313",
        password: "",
      }),
    };
    
    export default configPg;
    

    Хорошо, теперь можно создать файл миграции, чтобы можно было его отредактировать, задав, какую таблицу и какие данные мы собираемся создавать. Для этого вводим следующую команду:

    deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make create_todo
    

    Возможно, у неискушенного читателя уже возникли вопросы, зачем такое количество флагов. Это и есть secure в Deno — для выполнения команды нужно указать, какой уровень доступа вы ему даете ( это же и есть гениальная система безопасности, которую мы ждали ). Эти флаги и вызывают довольно острое горение у многих — их надо печатать? Дописывать bash скрипты, в которых ты вызываешь их всегда с полными правами? Мне думается, в какой-то момент эта система поменяется, но пока как есть. Таким образом, у нас в папке .migrations должен появится файл миграции, который вы сможете его отредактировать, как заходите. Я отредактировал его следующим образом:

    export const up = (): string => {
      return "CREATE TABLE todo (id serial, text text, done boolean)";
    };
    
    export const down = (): string => {
      return "DROP TABLE todo"
    };
    
    

    После этого мы можем выполнить миграции в базу данных с помощью следующей команды:

    deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts migrate
    

    В nessia есть более продвинутый вариант миграции, но я решил сделать все немного проще. Дальше мы можем переходить к созданию своей модели.

    Работаем с Моделью


    Создаем папку models, в которой у нас будут находится сами запросы. Для начала нужно создать файл config.ts, в котором будут сложены настройки подключения к базе (да, они у нас уже были упомянуты в nessie.config.ts, но тот файл у нас использовался под миграции). Для коннекта в файле config.ts мы будем использовать библиотеку deno-posgres. В итоге файл конфига будут выглядеть следующим образом:

    import { Client } from "https://deno.land/x/postgres/mod.ts";
    
    const client = new Client({
      user: "isakura313",
      database: "deno_crud",
      hostname: "localhost",
      port: 5432,
    });
    
    export default client;
    

    Отлично! Теперь переходим непосредственно к модели. Для построения sql -запросов я собираюсь воспользоваться библиотекой Dex, портом библиотеки Knex на Deno. Она позволит мне программно определять, какой sql — запрос я собираюсь выполнить. Начнем с определения, с каким диалектом Dex придется работать в этот раз, и определения интерфейса нашего todo:

    const dex = Dex({ client: "postgres" });
    
    interface Todo {
      id?: number;   //? - опциональный  параметр в TypeScript
      text: string;
      done: boolean;
    }
    

    Отлично, теперь можем приступать к самой мякотке. Для начала определим get запрос, который будет получать все todo. Я буду использовать асинхронное выполнение функции, чтобы через await соединиться с базой данных и получить результат запроса:

    async function getAllTodo() {
      await client.connect();
      const getQuery = dex.queryBuilder().select("*").from("todo").toString();
      const result = await client.query(getQuery);
      return result;
    }
    

    Если вы хоть немного знакомы в TypeScript и c async/await, здесь у вас вопросов не возникнет. Но дальше больше — нам нужно написать post-запрос, который будет добавлять дело в базу данных.

    async function addTodo(todo: Todo) {
      await client.connect();
      const insertQuery = dex.queryBuilder().insert([todo]).into("todo").toString();
      return client.query(insertQuery).then(async () => {
        const getQuery = dex.queryBuilder().select("*").from("todo").where(
          { text: todo.text },
        ).toString();
        const result = await client.query(getQuery);
        const result_data = result.rows ? result.rows[0] : {};
        return result_data;
      });
    }
    
    

    Вышеприведённый код вполне можно сократить, однако я постарался сделать его максимально простым и выразительным, даже в ущерб количеству строк. В общем плане там происходит следующее — внутри функции у нас создается соединение с базой данных, выстраивается insert — запрос, происходит запрос, внутри которого создается новый запрос, который возвращает у нас только что созданное дело или пустой объект.

    Остальные функции — редактирования и удаления я приведу вместе. В редактировании у нас происходит примерно то же самое, что и в добавлении дела, за исключением того, что в editTodo у нас происходит update. В delete у нас просто удаляется дело и ничего не возвращается:

    async function editTodo(id: number, todo: Todo) {
      await client.connect();
      const editQuery = dex.queryBuilder().from("todo").update(todo).where({ id })
        .toString();
      return client.query(editQuery).then(async () => {
        const getQuery = dex.queryBuilder().select("*").from("todo").where(
          { text: todo.text },
        ).toString();
        const result = await client.query(getQuery);
        const result_data = result.rows ? result.rows[0] : {};
        return result_data;
      });
    }
    
    async function deleteTodo(id: number) {
      await client.connect();
      const deleteQuery = dex.queryBuilder().from("todo").delete().where({ id })
        .toString();
      return client.query(deleteQuery);
    }
    

    Здорово. Осталось только экспортнуть наши функции, и можно приступать к настройке работы сервера и роутингу:

    export {
      addTodo,
      getAllTodo,
      editTodo,
      deleteTodo,
    };
    
    

    Работа роутинга и сервера


    Создаем папку routes, в которой у нас будет routes.ts. Для работы нашего routes мы воспользуемся denotrain, библиотекой, которая была вдохновлена expressJS и позволяется работать с url — запросами, формировать роутинг и множество всего полезного. Мы импортируем наши функции и Router:

    import { Router } from "https://deno.land/x/denotrain@v0.5.0/mod.ts";
    import { addTodo, getAllTodo, editTodo, deleteTodo } from "../models/models.ts";
    
    const api = new Router();
    

    Добавим методы get и post:

    api.get("/", (ctx) => {
      return getAllTodo().then((result) => {
        return result.rows;
        //Возвращаем результат
      });
    });
    
    api.post("/", (ctx) => {
      const body = {
      //формируем тело запроса
        text: ctx.req.body.text,
        done: ctx.req.body.done,
      };
    
      return addTodo(body).then((newTodo) => {
        ctx.res.setStatus(201); // возвращаем код "Created"
        return newTodo;
      });
    });
    

    Ctx — это переменная, которая отвечает у нас за контекст ответа. Не знаю, что добавить к комментариями, по моему, все и так очевидно. Осталось только добавить только методы patch и delete и экспортировать наше api:

    api.patch("/:id", (ctx) => {
      const todo = {
        text: ctx.req.body.text,
        done: ctx.req.body.done,
      };
    
      return editTodo(ctx.req.params.id as number, todo).then((result) => {
        return result;
      });
    });
    
    api.delete("/:id", (ctx) => {
      return deleteTodo(ctx.req.params.id as number).then(() => {
        ctx.res.setStatus(204);
        return true;
      });
    });
    
    export default api;
    
    

    Осталось только поднять наш сервак. В корневой папке создаем файл server.ts, в котором мы тоже импортируем Application из denotrain, чтобы поднять наш сервер:

    import {Application} from "https://deno.land/x/denotrain@v0.5.0/mod.ts";
    import api from "./routes/routes.ts";
    
    const app = new Application({port: 1337}) // поднимаем наш сервер на порту 1337
    
    app.use("/api/todos", api) // опредеяем адрес, по котором можно будет осуществлять запросы
    app.run() // запускаем приложение
     

    Вполне возможно, что у вас возникнут проблемы в работе приложения. Каждый раз перезапускать вручную компилирующийся сервер совсем не хочется, поэтому я использую аналог nodemon — Denomon, который вполне добросовестно будет перекомплировать ваш код и перезапускать сервер. В нашем случае подойдет следующая команда:

    denomon --allow net,read server.ts
     

    Мои впечатления

    Мне очень понравилось писать на Deno. Несмотря на некий скептицизм в его сторону, с помощью TypeScript и грамотно выстроенной среды разработки можно достигнуть больших результатов в написании высоконадежного сервиса, который будет работать максимально предсказуемого. Еще есть куда расти (к примеру, в скорости перекомпиляции проекта) и пока рано пытаться применять Deno на серьезном продакшене, но я надеюсь динозавр сможет перенять самое лучшее от Node.js, немного изменить странности архитектуры, и продолжить свое развитие.

    На этом в создании нашего API все. Весь проект целиком вы можете найти здесь. Можете открыть Postman и проверить, что все работает корректно. По традиции, несколько полезных ссылок:

    REST микрофреймворк для Deno без зависимостей
    ejs-engine для Deno
    Официальная документация к Deno
    сервер Deno в Discord
    Создание Chat App на Deno + React
    Прекрасное введение в Deno на русском



    Узнать подробнее о курсе


    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Похожие публикации

    Комментарии 1

      –1
      > Любопытный факт, что по внешнему ip встроенный сервер PHP 7.4 смог соединиться с моей базой MySQL, а Deno показывал packets out of order)

      … но вместо того, чтобы взять tcpdump и посмотреть что там — решил использовать postgresql. :facepalm: Ну такой себе разработчик

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

      Самое читаемое