CRUD API on Deno and PostegreSQL: working with a dinosaur

Hello everyone. In anticipation of the start of the course “Fullstack JavaScript Developer”, we want to share interesting material sent by our freelance author. This article is not related to the course program, but it will certainly be interesting as a short review on Deno.


“-Ryan, we’re thinking over the concept of CLI commands for Deno. How many flags for secure runtime to add?”
“-Yes”
“-Ryan, we need to come up with a symbol of language so that everyone understands that we are doing something direct new. Which animal will we choose?”
Dinosaur

Hello everyone. Deno is becoming more and more famous and popular, there are a huge number of new videos and text guides on this subject, but mainly in English. Several articles on Deno were also published on Habré, for example, here and here, but mainly articles talk about Deno in theoretical terms. Today we will try to create a small CRUD Api that can serve, for example, a todo application on the frontend, and I will share with you my impressions of this dinosaur in real coding.

Theoretical introduction

For those who are in the tank, I will tell you what Deno is, if you have not read, for example, one of the articles above. Deno is a runtime runtime that runs on JavaScript and TypeScript (TypeScript is supported out of the box, the TypeScript compiler version is hardwired to the Deno version, you can only change it if you change the Deno version), which is based on the V8 engine and Rust programming language. Deno was created by Ryan Dahl, the creator of Node.js, and Deno’s main qualities are performance and security. Deno was announced in 2018, and what many sources forget to mention – was greatly influenced by Golang (and indeed it was originally written in Golang, but later had to be rewritten in Rust).

The standard library was created on the model of the standard Go library, and many tools or their implementations switched from Golang to Deno, for example, the lack of an ecosystem like npm and pumping libraries directly from web resources at the initial build stage (which causes some kind of stupor in Node. js of developers who, quite naturally, were not familiar with a similar system with Go). So for whom is Deno now? If you like typed languages, like Golang ideas and tools, but want to write backing on something simpler – it’s time to try Deno in reality. However, if you want to wait for the platform to ripen, more libraries and answers to StackOverflow will appear, maybe you should wait a bit.

Start to create

Despite the youth of Deno, some kind of ecosystem managed to form around it (which, of course, cannot yet be compared with the Node.js ecosystem), which allows you to cover the basic needs for creating your own API. Let’s decide what we are creating: I want to create a simple CRUD Api using Deno and TypeScript on the backend (since there is TypeScript support, why not use it), PostgreSQL as a database (in general, I wanted to use MySQL, but accidentally broke my access on localhost and couldn’t fix it. A curious fact is that on the external ip, the built-in PHP 7.4 server was able to connect to my MySQL database, and Deno showed packets out of order), and our requests will look like this:

Method Routes Execution result
Get / api / todos / get Getting all todo
Post / api / todos / post Creating a new business
PATCH / api / todos /: id Updating a specific case
DELETE / api / todos /: id Delete case by id

Well, we decided on the query scheme, we launched the PostgreSQL server (if that you can download it from herevery nice GUI client Postico), we created the crud_api database.
What’s next? You can create a table with your hands using sql queries or a client, of course, but I want to see how it is with Deno’s migrations. In this case, we need a library nessia, allowing you to create migrations similar to migrations in Laravel. But before we go into migration, let’s talk about organizing a development environment.

Just the other day, the official JetBrains Deno development plugin was released. You can find him here. On Visual Studio you need to install the Deno plugin, and create a settings folder in the project folder .vscode, where in settings.json you turn on deno support (but it seemed to work for me without it):

    "deno.enanble":true

That’s all with the preparatory stage, we can return to migrations. First we need to enter the initialization command, taking it from their library documentation:

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

We create a configuration file for our library, with the help of which we can determine which DBMS and database you are going to connect to.
Initially, three templates are created in the file – for PostgreSQL, MySQL and Sqlite (by default, settings for PostegreSQL are exported).

As a result, our file nessie.config.ts will look like this:

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;

Well, now you can create a migration file so that you can edit it by specifying which table and what data we are going to create. To do this, enter the following command:

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

Perhaps the inexperienced reader has already had questions about why so many flags. This is secure in Deno – to execute the command you need to specify what level of access you give it ( this is the ingenious security system that we were waiting for ) These flags cause quite sharp burning in many – do they need to be printed? Append bash scripts in which you always invoke them with full rights? I think at some point this system will change, but for now, as it is. So in our folder .migrations A migration file should appear, which you can edit as you go. I edited it as follows:

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

export const down = (): string => {
  return "DROP TABLE todo"
};

After that, we can migrate to the database using the following command:

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

There is a more advanced migration option in nessia, but I decided to make things a little easier. Further we can proceed to create our own model.

We work with the Model

Create a folder modelsin which we will have the requests themselves. First you need to create a file config.ts, in which the settings for connecting to the database will be added (yes, we have already mentioned them in nessie.config.ts, but that file was used for migration). For a connection in a file config.ts we will use the library deno-posgres. As a result, the config file will look like this:

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;

Fine! Now we pass directly to the model. To build sql queries I’m going to use the library Dex, a port of the Knex library on Deno. It will allow me to programmatically determine which sql query I’m going to execute. Let’s start by defining which dialect Dex will have to work with this time, and defining the interface of our todo:

const dex = Dex({ client: "postgres" });

interface Todo {
  id?: number;   //? - опциональный  параметр в TypeScript
  text: string;
  done: boolean;
}

Well, now we can proceed to the pulp itself. First, define a get request that will receive all todo. I will use asynchronous function execution to connect to the database via await and get the result of the query:

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

If you are even a little familiar with TypeScript and c async / await, you won’t have any questions here. But further more – we need to write a post-request that will add the case to the database.

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;
  });
}

The above code can be shortened, however, I tried to make it as simple and expressive as possible, even to the detriment of the number of lines. In general terms, the following happens there – inside the function, we create a connection to the database, insert is built in – the request, a request occurs, inside which a new request is created, which returns the newly created case or an empty object.

Other functions – editing and deleting, I will give together. In editing, the same thing happens for us as in adding a case, except that we have an update in editTodo. In delete, we simply delete the case and return nothing:

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);
}

Wow. It remains only to export our functions, and you can begin to configure the server and routing:

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

Routing and server operation

Create the routes folder in which we will have routes.ts. We will use our routes denotrain, a library that was inspired by expressJS and is allowed to work with url requests, configure routing and a lot of useful stuff. We import our functions and 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();

Add the get and post methods:

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 is the variable that is responsible for the response context. I don’t know what to add to the comments, in my opinion, everything is already so obvious. It remains only to add the patch and delete methods and export our 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;

It remains only to raise our server. In the root folder, create a file server.ts, in which we also import Application from denotrain to raise our server:

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() // запускаем приложение
 

It is possible that you will have problems in the application. I do not want to restart the compiling server manually each time, so I use the analog nodemon – Denomonwhich in good faith will recompile your code and restart the server. In our case, the following command is suitable:

denomon --allow net,read server.ts
 
My impressions

I really enjoyed writing on Deno. Despite some skepticism in his direction, with the help of TypeScript and a well-built development environment, you can achieve great results in writing a highly reliable service that will work as predictable as possible. There is still room to grow (for example, in the speed of recompiling the project) and it’s too early to try to use Deno for serious production, but I hope the dinosaur will be able to adopt the best from Node.js, slightly change the oddities of architecture, and continue its development.

That’s it in creating our API. The whole project you can find here. You can open Postman and verify that everything is working correctly. By tradition, a few useful links:

REST microframework for Deno without dependencies
ejs-engine for Deno
Official Deno documentation
Deno server in Discord
Creating a Chat App on Deno + React
A great introduction to Deno in Russian


Learn more about the course


Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *