Создание интерфейсов прикладного программирования (API) — одна из задач, которую разработчикам приходится решать время от времени. Веб-разработчики используют различные инструменты для выполнения этих задач, которые необходимы в их повседневном графике.
В этом руководстве мы рассмотрим, как создать простой API для электронной коммерции с использованием PostgreSQL, Express.js и Sequelize CLI.
Вот что мы рассмотрим:
- Что такое Sequelize?
- Установка Sequelize CLI
- Создание моделей сущностей с помощью CLI
- Написание модульных тестов с помощью Jest
- Развертывание на Heroku
- Создание ассоциаций с помощью Postgres и Sequelize CLI
- CI/CD с использованием Circle CI.
Что нужно знать, прежде чем читать дальше
- Базовые знания о SQL
- Основы Express.js
- HTTP методы
Чтобы следовать этому руководству, на вашей машине для разработки должны быть установлены следующие инструменты:
- Nodejs 14x или выше (я использую v14.17.3)
- Yarn или NPM (я использую Yarn v1.22.15)
- Текстовый редактор (я использую VS Code)
- Локальная установка PostgreSQL
Что такое Sequelize?
Sequelize — это простой в использовании инструмент объектно-реляционного отображения (ORM) на JavaScript, который работает с базами данных SQL.
Настройка проекта
Установка Sequelize
Создайте новый каталог, который вы будете использовать во время работы с этим руководством, и установите зависимости проекта.
mkdir sequelize-tutorial
cd sequelize-tutorial
yarn add sequelize cors pg morgan helmet && yarn add -D sequelize-cli nodemon dotenv
- sequelize — Sequelize — это основанный на обещаниях Node.js ORM инструмент для Postgres. В нем есть поддержка транзакций, отношения, нетерпеливая и ленивая загрузка, репликация чтения и многое другое.
- cors — CORS — это пакет node.js для обеспечения промежуточного ПО Connect/Express, которое может быть использовано для включения CORS с различными опциями.
- pg — Неблокирующий клиент PostgreSQL для Node.js.
- morgan — Промежуточное ПО для регистрации HTTP-запросов для node.js.
- helmet — Helmet помогает вам защитить ваши приложения Express путем установки различных HTTP-заголовков.
- sequelize-cli — интерфейс командной строки (CLI) Sequelize.
- nodemon — инструмент, который помогает разрабатывать приложения на базе node.js, автоматически перезапуская приложение node при обнаружении изменений файлов в каталоге.
- dotenv — модуль, загружающий переменные окружения из файла
.env
вprocess.env
.
Добавьте файлы .gitignore
и .env
в корень папки вашего проекта.
touch .gitignore .env
Затем добавьте папку node_modules
и файл .env
в папку .gitignore
.
/node_modules
.env
Прежде чем мы начнем
Создание наших сущностей
Наш API имеет две сущности
- Продукт
- Категория
Отношения
- Один
продукт
принадлежит однойкатегории
. - Одна
категория
имеет многопродуктов
.
Давайте перейдем непосредственно к коду. ?
- Инициализируйте проект Sequelize, затем откройте каталог в нашем редакторе кода (VS Code):
yarn sequelize-cli init
code .
- Настройте проект на использование базы данных Postgres в качестве диалекта SQL. Перейдите к
config.json
в директории/config
и измените код на следующий:
{
"development": {
"username": "postgres",
"password": null,
"database": "db_dev",
"host": "127.0.0.1",
"dialect": "postgres"
},
"test": {
"username": "postgres",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "postgres"
},
"production": {
"use_env_variable": "DATABASE_URL",
"dialect": "postgres",
"dialectOptions": {
"ssl": {
"rejectUnauthorized": false
}
}
}
}
Поскольку мы все еще находимся в режиме разработки проекта, добавьте поля имени пользователя, базы данных и пароля в объект development
.
В моем случае имя пользователя — postgres
, пароль — null
(я не использую пароль — плохая практика, я знаю) и база данных — db_dev
.
- После выполнения вышеописанного шага создайте базу данных для проекта с помощью этой команды:
yarn sequelize-cli db:create
Определение моделей
Наша цель — связать продукты с категориями, к которым они относятся.
Давайте создадим модель Product
.
$ yarn sequelize-cli model:generate --name Product --attribute name:string,quantity:integer,inStock:boolean,productImage:string,expiryDate:date
Ниже приведено содержимое созданного файла миграции.
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Products', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
},
name: {
type: Sequelize.STRING,
trim: true,
},
quantity: {
type: Sequelize.INTEGER,
trim: true,
allowNull: false,
},
price: {
type: Sequelize.INTEGER,
allowNull: false,
trim: true,
},
inStock: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
productImage: {
type: Sequelize.STRING,
allowNull: false,
trim: true,
},
expiryDate: {
type: Sequelize.DATE,
allowNull: false,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Products');
},
};
Обратите внимание, что я использую UUID
вместо типа данных integer
для поля id
.
О том, как использовать UUID, читайте здесь.
Вот статья о том, как добавить расширение к вашей базе данных с помощью оболочки PSQL
Когда вы выполняете команду model:generate
, Sequelize генерирует файл модели и миграцию с указанными атрибутами.
Теперь мы можем запустить миграцию, чтобы создать таблицу Products
в нашей базе данных.
yarn sequelize-cli db:migrate
Давайте сгенерируем seed-файл для модели Product
.
yarn sequelize-cli seed:generate --name product
Новый файл с именем -product.js
был создан в папке /seeders
. Скопируйте и вставьте приведенный ниже код, который является образцом данных продуктов.
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkInsert(
'Products',
[
{
id: '1373772c-125c-42d0-81ae-a6d020fcbe21',
name: 'Bread',
quantity: 4,
inStock: true,
productImage:
'https://res.cloudinary.com/morelmiles/image/upload/v1649765314/download_nwfpru.jpg',
// Store the price in cents e.g if price is $5, multiply by 100 cents e.g 5 * 100 = 500 cents
price: 500,
expiryDate: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '9df55a7c-772c-459f-a21b-933a96981ca6',
name: 'Milk',
quantity: 4,
inStock: true,
productImage:
'https://res.cloudinary.com/morelmiles/image/upload/v1647356184/milk_ckku96.jpg',
// Store the price in cents e.g if the price is $5, multiply by 100 cents e.g 5 * 100 = 500 cents
price: 100,
expiryDate: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete('Products', null, {});
},
};
Это все для модели Products
на данный момент.
Создайте модель Category
$ yarn sequelize-cli model:generate --name Category --attributes name:string
Ниже приведен файл миграции для модели Category
.
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Categories', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
},
name: {
type: Sequelize.STRING,
trim: true,
allowNull: false,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Categories');
},
};
Давайте создадим файл посева для модели Category
.
yarn sequelize-cli seed:generate --name category
Ниже приведен файл с записями сеялки.
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkInsert(
'Categories',
[
{
id: 'a52467a3-3a71-45c4-bf1c-9ace5ad3668f',
name: 'Confectionaries',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '33a9e6e0-9395-4f6c-b1cd-3cf1f87e195a',
name: 'Drinks',
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete('Categories', null, {});
},
};
Теперь мы можем создать ассоциации между Products
и Category
, к которой каждый из них принадлежит.
Подробнее об ассоциациях: здесь
Замените код в директории models/product
на приведенный ниже:
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Product extends Model {
static associate(models) {
Product.belongsTo(models.Category, {
foreignKey: 'categoryId',
});
}
}
Product.init(
{
name: DataTypes.STRING,
quantity: DataTypes.INTEGER,
inStock: DataTypes.BOOLEAN,
productImage: DataTypes.STRING,
price: DataTypes.INTEGER,
expiryDate: DataTypes.DATE,
},
{
sequelize,
modelName: 'Product',
}
);
return Product;
};
Замените код в каталоге models/category
на приведенный ниже:
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Category extends Model {
static associate(models) {
Category.hasMany(models.Product, {
foreignKey: 'categoryId',
onDelete: 'CASCADE',
});
}
}
Category.init(
{
name: DataTypes.STRING,
},
{
sequelize,
modelName: 'Category',
}
);
return Category;
};
Теперь добавим внешний ключ в файл миграции Product
.
Перейдите в подпапку /migrations
и добавьте приведенный ниже код в файл, который заканчивается create-product.js
:
categoryId: {
type: Sequelize.UUID,
allowNull: false,
onDelete: 'CASCADE',
references: {
model: 'Categories',
key: 'id',
as: 'categoryId'
}
},
Приведенная выше строка кода добавляет внешний ключ categoryId
к таблице Products
в нашей базе данных.
Чтобы добавить колонку categoryId
в таблицу Products
, проверьте скрипты в файле package.json
.
В файле package.json
есть скрипт под названием db:reset
. Видели его? Он поможет нам сбросить базу данных, создать базу данных снова, запустить миграции и добавить данные сеялок в нашу базу данных.
Запустите его.
yarn db:reset
Если вы откроете PSQL и запустите SELECT * FROM "Products";
, вы увидите обновленную таблицу Products
.
Теперь у нас есть таблица продуктов и категорий, давайте рассмотрим, как делать запросы и получать ответы с помощью Express.js.
Мы уже установили наши зависимости. Если вы еще не установили, сделайте это сейчас:
yarn add cors express helmet morgan && yarn add -D dotenv nodemon
Добавление контроллеров.
Создайте новый каталог с именем controllers
и создайте три файла: category.js
, index.js
и product.js
.
touch category.js index.js product.js
Откройте category.js
в папке controllers и вставьте приведенный ниже код.
const { Category, Product } = require('../models');
/**
* Creates a new category
* @param {*} req
* @param {*} res
* @returns Object
*/
const createCategory = async (req, res) => {
try {
const category = await Category.create(req.body);
return res.status(201).json({
category,
});
} catch (error) {
return res.status(500).json({ error: error.message });
}
};
/**
* Fetches all categories
* @param {*} req
* @param {*} res
* @returns Object
*/
const getAllCategories = async (req, res) => {
try {
const categories = await Category.findAll({
order: [['createdAt', 'DESC']],
});
return res.status(200).json({ categories });
} catch (error) {
return res.status(500).send(error.message);
}
};
/**
* Gets a single category by it's id
* @param {*} req
* @param {*} res
* @returns boolean
*/
const getCategoryById = async (req, res) => {
try {
const { id } = req.params;
const category = await Category.findOne({
where: { id: id },
order: [['createdAt', 'DESC']],
});
if (category) {
return res.status(200).json({ category });
}
return res
.status(404)
.send('Category with the specified ID does not exist');
} catch (error) {
return res.status(500).send(error.message);
}
};
/**
* Updates a single category by it's id
* @param {*} req
* @param {*} res
* @returns boolean
*/
const updateCategory = async (req, res) => {
try {
const { id } = req.params;
const [updated] = await Category.update(req.body, { where: { id: id } });
if (updated) {
const updatedCategory = await Category.findOne({
where: { id: id },
include: [
{
model: Product,
},
],
});
return res.status(200).json({ category: updatedCategory });
}
throw new Error('Category not found ');
} catch (error) {
return res.status(500).send(error.message);
}
};
/**
* Deletes a single category by it's id
* @param {*} req
* @param {*} res
* @returns Boolean
*/
const deleteCategory = async (req, res) => {
try {
const { id } = req.params;
const deleted = await Category.destroy({
where: {
id: id,
},
});
if (deleted) {
return res.status(204).send('Category deleted');
}
throw new Error('Category not found ');
} catch (error) {
return res.status(500).send(error.message);
}
};
module.exports = {
createCategory,
getAllCategories,
getCategoryById,
updateCategory,
deleteCategory,
};
Теперь перейдите к файлу product.js
и вставьте следующий код:
const { Product } = require('../models');
/**
* Creates a new product
* @param {*} req
* @param {*} res
* @returns Object
*/
const createProduct = async (req, res) => {
try {
const product = await Product.create(req.body);
return res.status(201).json({
product,
});
} catch (error) {
return res.status(500).json({ error: error.message });
}
};
/**
* Fetches all products
* @param {*} req
* @param {*} res
* @returns Object
*/
const getAllProducts = async (req, res) => {
try {
const products = await Product.findAll({ order: [['createdAt', 'DESC']] });
return res.status(200).json({ products });
} catch (error) {
return res.status(500).send(error.message);
}
};
/**
* Gets a single product by it's id
* @param {*} req
* @param {*} res
* @returns boolean
*/
const getProductById = async (req, res) => {
try {
const { id } = req.params;
const product = await Product.findOne({
where: { id: id },
});
if (product) {
return res.status(200).json({ product });
}
return res.status(404).send('Product with the specified ID does not exist');
} catch (error) {
return res.status(500).send(error.message);
}
};
/**
* Updates a single product by it's id
* @param {*} req
* @param {*} res
* @returns boolean
*/
const updateProductById = async (req, res) => {
try {
const { id } = req.params;
const product = await Product.update(req.body, {
where: { id: id },
});
if (product) {
const updatedProduct = await Product.findOne({ where: { id: id } });
return res.status(200).json({ product: updatedProduct });
}
throw new Error('product not found');
} catch (error) {
return res.status(500).send(error.message);
}
};
/**
* Deletes a single product by it's id
* @param {*} req
* @param {*} res
* @returns boolean
*/
const deleteProductById = async (req, res) => {
try {
const { id } = req.params;
const deletedProduct = await Product.destroy({
where: { id: id },
});
if (deletedProduct) {
return res.status(204).send('Product deleted successfully ');
}
throw new Error('Product not found');
} catch (error) {
return res.status(500).send(error.message);
}
};
module.exports = {
createProduct,
getAllProducts,
getProductById,
deleteProductById,
updateProductById,
};
Откройте файл index.js
в текстовом редакторе и вставьте следующий код:
//Exports the entity controllers in a single object
const productController = require('./product');
const categoryController = require('./category');
module.exports = {
productController,
categoryController,
};
Отлично! Молодцы, что дочитали до конца…
Теперь мы собираемся создать маршруты для различных операций контроллера (я имею в виду HTTP методы ?).
Создайте новую директорию и назовите ее routes
. Внутри директории routes создайте новый файл с именем index.js
.
mkdir routes
cd routes
touch index.js
Внутри каталога routes/index.js
вставьте следующий код .
const router = require('express').Router();
// Controller imports
const { categoryController, productController } = require('../controllers');
// Category routes
router.get('/v1/categories', categoryController.getAllCategories);
router.post('/v1/categories', categoryController.createCategory);
router.get('/v1/categories/:id', categoryController.getCategoryById);
router.put('/v1/categories/:id', categoryController.updateCategory);
router.delete('/v1/categories/:id', categoryController.deleteCategory);
// Product routes
router.get('/v1/products', productController.getAllProducts);
router.post('/v1/products', productController.createProduct);
router.get('/v1/products/:id', productController.getProductById);
router.put('/v1/products/:id', productController.updateProductById);
router.delete('/v1/products/:id', productController.deleteProductById);
module.exports = router;
Теперь вы можете протестировать ваши конечные точки с помощью любого клиента, который вы выберете. Популярными являются Insomnia и Postman.
Я использую Postman.
[ПРОДОЛЖЕНИЕ СЛЕДУЕТ]