В этом блоге я покажу вам, как создать базовый REST API с помощью Node.js и PostgreSQL.

Мы дополнительно будем использовать express и node-postgres. В этом блоге не будет использоваться ORM, например Sequelize, чтобы мы могли лучше понять запрашиваемую часть.

Наше приложение просто зарегистрирует пользователей и добавит их в нашу базу данных.

Создание базы проекта

В выбранном вами каталоге сгенерируйте проект голого узла:

npm init -y

Давайте теперь установим экспресс:

npm install express

Отлично, теперь мы можем запустить базовый сервер. Создадим основной файл app.js:

const express = require('express');
const app = express();
app.use(express.json());
// we will eventually use env variables
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log('Server Started');
});

Давайте сейчас создадим нашу таблицу users. Для этого убедитесь, что в вашей системе установлен и запущен PostgreSQL. Мы также будем использовать pg-admin для взаимодействия с этой таблицей.

Давайте сначала создадим эту таблицу. В pg-admin щелкните правой кнопкой мыши базу данных с вашим именем пользователя (или другой базой данных по вашему выбору) и выберите Инструмент запросов:

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

CREATE TABLE users(
  id SERIAL PRIMARY KEY,
  username VARCHAR(40) NOT NULL UNIQUE, 
  email VARCHAR(50) NOT NULL UNIQUE,
  password VARCHAR(100) NOT NULL,
  bio VARCHAR(400) NOT NULL DEFAULT '',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

Обратите внимание, что имя пользователя и адрес электронной почты всегда должны быть уникальными. Теперь эта таблица должна быть видна на боковой панели pg-admin:

Начинаем работу над нашим приложением node

Добавление node-postgres

Вернемся в каталог вашего проекта, давайте начнем с создания подключения к нашей базе данных. Для этого воспользуемся модулем node-postgres.

npm i pg

Затем создайте новый каталог с именем db и добавьте в него файл index.js.

Этот файл будет содержать нашу конфигурацию пула соединений, которую мы можем импортировать и использовать в нашем проекте.

Откройте файл db / index.js и добавьте следующие строки:

const { Pool } = require('pg');
const pool = new Pool();

Мы будем использовать пул соединений для взаимодействия с нашей базой данных в этом блоге.

Мы используем пул соединений вместо подключений с одним клиентом из-за тех преимуществ, которые он предлагает. Но как нам подключить этот пул к нашей базе данных?

Добавление конфигурации для подключения к нашей базе данных

Чтобы установить соединение с нашей базой данных, нам понадобится следующая информация:

  • База данных пользователь (по умолчанию ваше системное имя пользователя)
  • База данных хост (где запущена база данных)
  • База данных порт (по умолчанию 5432)
  • имя базы данных (по умолчанию ваше имя пользователя postgres).
  • пароль базы данных.

Эту информацию должно быть легко найти для вашей локальной конфигурации.

Реальные проекты часто используют переменные среды для получения этой информации. Поэтому мы создадим файл .env со всей этой информацией в основном каталоге вашего проекта.

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

PGUSER='arunpant'
PGHOST='localhost'
PGDATABASE='arunpant'
PGPASSWORD='mySuperSecretPassword'
PGPORT=5432
PORT=8083

Если вы помните наш файл app.js:

const PORT = process.env.PORT || 8080;

Чтобы убедиться, что переменные среды, такие как PORT, подобраны, мы будем использовать npm-пакет dotenv.

npm i dotenv

И в app.js:

const express = require('express');
const app = express();
require('dotenv').config();
...

Создание генератора запросов

Вернемся к нашему файлу db / index.js, чтобы поработать над остальным содержимым. Мы создадим функцию запроса, которую сможем экспортировать в другие места в нашем приложении для выполнения запросов.

const { Pool } = require('pg');
const pool = new Pool();
module.exports = {
    async query(text, params) {
      const res = await pool.query(text, params);
      return res;
    }
};
// text will be something like 'SELECT * FROM $1'
// params something like this array: ['users'] i.e. the table name
// $1 => replaced by users in final query

text будет нашим запросом, а params будут динамическими элементами этого запроса, которые добавляются отдельно для предотвращения атак с использованием SQL-инъекций.

Поскольку операция pool.query может завершиться неудачно по ряду причин, например, при нарушении ограничений, давайте также добавим базовую обработку ошибок и ведение журнала:

const { Pool } = require('pg');
const pool = new Pool();
module.exports = {
    async query(text, params) {
        // invocation timestamp for the query method
        const start = Date.now();
        try {
            const res = await pool.query(text, params);
            // time elapsed since invocation to execution
            const duration = Date.now() - start;
            console.log(
              'executed query', 
              {text, duration, rows: res.rowCount}
            );
            return res;
        } catch (error) {
            console.log('error in query', {text});
            throw error;
        }
    }
};

Мы инкапсулировали наш код в блок try catch и добавили базовое ведение журнала.

Node-postgres добавит метаданные ошибки к выданной ошибке. Вскоре мы будем использовать один из таких кодов ошибки, чтобы идентифицировать нарушение ограничения UNIQUE для имен пользователей.

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

Создание потока для регистрации пользователя

Мы создадим в нашем проекте папку models и добавим в нее файл User.js.

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

function User ({
  username, 
  email, 
  password, 
  bio=`Hi, I am ${username}`
}) {
    this.username = username;
    this.email = email;
    this.password = password;
    this.bio = bio;
};

Теперь мы добавим к его прототипу метод для фактического создания пользователя:

const db = require('../db');
//User constructor
function User ({
  username, 
  email, 
  password, 
  bio=`Hi, I am ${username}`
}) {
    this.username = username;
    this.email = email;
    this.password = password;
    this.bio = bio;
};
// add a createUser method to the prototype
User.prototype.createUser = async function() {
    try {
        const { rows } = await db.query(
            `INSERT INTO users(username, email, password, bio) 
            VALUES ($1, $2, $3, $4)`,
            [this.username, this.email, this.password, this.bio]
        );
        return rows; 
    } catch (error) {
        throw error;
    }
};
module.exports = User;
// db.query: the query method we exported earlier from db/index.js

По умолчанию node-postgres считывает строки и собирает их в объекты JavaScript с ключами, соответствующими именам столбцов, и значениями, соответствующими значению соответствующей строки для каждого столбца.

Деструктурированный объект {rows} будет содержать эту информацию, если таковая имеется. Он будет содержать массив объектов для всех совпадающих строк для определенного запроса.

Благодаря этому у нас есть наша базовая модель User.

Создание контроллера для регистрации пользователей

Давайте создадим в нашем проекте еще одну папку с именем controllers и добавим в нее файл users.js.

Добавим в этот файл код контроллера.

const User = require('../models/User');
exports.postSignup = async (req, res, next) => {
    //getting user data from request body
    const {username, password, email, bio} = req.body;
    try {
        const user = new User({username, password, email, bio});
        const result = await user.createUser();
        res.send(user);
    } catch (error) {
        next(error);
    }
};
Remember the createUser method we added to the User model prototype? It is therefore available on our user model instance above.

user. createUser () выполнит наш запрос на вставку пользователя. Этот запрос может завершиться ошибкой, например, в случае нарушения ограничения UNIQUE. У нас есть ограничение UNIQUE для имени пользователя, и если кто-то попытается создать пользователя с тем же именем пользователя, метод createUser выдаст ошибку.

Эта ошибка будет содержать метаданные для этого конкретного вида нарушения ограничения, включая код, который мы можем использовать, чтобы определить, что это действительно было УНИКАЛЬНОЕ нарушение ограничения.

Postgres поставляется с конкретными кодами ошибок, список которых вы можете просмотреть здесь.

Вот фрагмент некоторых кодов ошибок нарушения ограничения целостности:

Class 23 — Integrity Constraint Violation
23000 integrity_constraint_violation
23001 restrict_violation
23502 not_null_violation
23503 foreign_key_violation
23505 unique_violation
23514 check_violation
23P01 exclusion_violation

Мы не будем рассматривать все сценарии ошибок, учитывая объем этого блога, поэтому давайте просто добавим случай, когда ограничение UNIQUE нарушается.

Вернувшись в контроллер, измените код следующим образом:

...
exports.postSignup = async (req, res, next) => {
    const {username, password, email, bio} = req.body;
    try {
        const user = new User({username, password, email, bio});
        const result = await user.createUser();
        res.send(user);
    } catch (error) {
        const errorToThrow = new Error();
        switch (error?.code) {
            case '23505':
                errorToThrow.message = 'User already exists';
                errorToThrow.statusCode = 403;
                break;
            default:
                errorToThrow.statusCode = 500;
        }
        //pass error to next()
        next(errorToThrow);
    }
};

Когда операция запроса приводит к ошибке, к объекту ошибки добавляется конкретный код ошибки postgres (в данном случае 23505).

Помните, мы проверяли нарушение именно этого кода:

23505 unique_violation

Наш блок catch будет просто next () эту ошибку, и мы можем добавить знакомый экспресс-обработчик ошибок, чтобы перехватывать такие ошибки с помощью свойства message и statusCode в главном app.js файл:

...
app.use((err, req, res, next) => {
    if(err.statusCode) {
        res.status(err.statusCode).send(err.message);
    } else {
        console.log(err);
        res.status(500).send('Something unexpected happened');
    }
});
const PORT = process.env.PORT || 8080;
...

Теперь мы создадим маршрут, который использует этот контроллер, что довольно просто.

Создайте в своем проекте папку с названием routes и добавьте в нее файл users.js:

const express = require('express');
const router = express.Router();
const usersController = require('../controllers/users');
router.post('/signup', usersController.postSignup);
module.exports = router;

И мы можем зарегистрировать этот маршрут в нашем файле app.js:

...
const usersRoute = require('./routes/users');
app.use(usersRoute);
...

Теперь мы можем проверить, работает ли наш API так, как задумано, с помощью Postman.

Проверка поведения API

Давайте сначала запустим проект:

node app.js

Давайте сделаем POST-запрос к маршруту «/ signup» с помощью почтальона для нового пользователя:

Как видите, мы сделали запрос на http: // localhost: 8083 / signup с полезной нагрузкой с нашим именем пользователя, адресом электронной почты и паролем.

Если вы помните, в нашей модели пользователя мы добавляли значение биографии по умолчанию, если оно не было предоставлено.

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

Если все прошло нормально, вы должны увидеть ответ выше с кодом статуса 403 и добавленное нами сообщение «Пользователь уже существует». Журналы вашего терминала также будут содержать информацию об ошибках.

Я надеюсь, что после настройки этого базового API вы сможете перейти к созданию более сложных API. Например, для проверки следующего потока входа в систему.

Надеюсь, этот блог помог вам хоть немного. Всего наилучшего для ваших будущих приключений в области программирования!