Структура backend
September 2, 2022 · View on GitHub
Ниже указаны потенциальные места добавления кода в проекте
┌ <projectRoot>
├─┬ src
│ ├─┬ database
│ │ ├─ controllers <- Контроллеры БД
│ │ ├─ migrations
│ │ └─ models <- Модели таблиц
│ └─┬ server
│ ├─ controllers <- Контроллеры express, "бизнес логика"
│ ├─ middlewares <- Общие
│ └─ routes <- Логика для отдельных "ручек"
1. Контроллеры БД
Занимаются подготовкой данные к записи в БД и обработкой прочитанных из БД данных.
Пример контроллера возвращающего пользователей:
// src/database/controllers/userController.ts
import { User } from '@/database/models';
const getUserById = (id: number): Promise<User | null> =>
User.findOne({ where: { id } });
export default {
getUserById,
};
Подключение осуществляется в src/database/controllers/index.ts
// src/database/controllers/index.ts
// ....
export { default as dbUserController } from './userController';
};
2. Модели таблиц БД
Содержат описание таблиц БД. Подробнее про конфигурацию моделей см. доку sequelize: по моделям по TypeScript, по типам дынных, по опциям колонок.
Также в каждом классе моделей необходимо определить статическую функцию registration(sequelize: Sequelize) => void.
Пример описания таблицы пользователей:
// src/database/models/User.ts
/* eslint-disable no-use-before-define */
import type {
CreationOptional,
InferAttributes,
InferCreationAttributes,
Sequelize,
ModelStatic,
} from 'sequelize';
import { DataTypes, Model } from 'sequelize';
const modelName = 'User';
const tableName = 'Users';
export default class User extends Model<
InferAttributes<User>,
InferCreationAttributes<User>
> {
declare id: CreationOptional<number>; // Поле опционально при создании записи
declare firstName: string;
declare secondName: string;
declare avatar: CreationOptional<string>; // Поле опционально при создании записи
// Обязательный метод, для регистрации модели
static registration = (sequelize: Sequelize) => {
User.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
first_name: {
type: DataTypes.STRING,
},
second_name: {
type: DataTypes.STRING,
},
avatar: {
type: DataTypes.STRING,
defaultValue: '',
},
},
{
sequelize,
modelName, // Указывать обязательно, иначе при prod сборке webpack заменит на случайные переменные
tableName, // Указывать обязательно, иначе при prod сборке webpack заменит на случайные переменные
/* другие опции модели */
}
);
};
}
// Функция проверки является ли переданная модель моделью пользователя
export const isUserMode = (mode: ModelStatic<Model>): mode is typeof User => {
return mode.tableName === User.tableName;
};
Подключение осуществляется в src/database/models/index.ts
// src/database/models/index.ts
// ....
export { default as User, isUserMode } from './User';
};
3. Миграции
Возможно несколько вариантов написания миграций. Типовой файл миграции должен именоваться по конвенции XXXX-<Описание>.ts, где XXXX - порядковый номер миграции.
Типовой файл миграции имеет вид:
// src/database/migrations/0001-first-migration.ts
import type { TUmugMigrationFn } from '@/database/types';
export const up: TUmugMigrationFn = async ({
context: { queryInterface, sequelize, transaction },
}) => {
// Описание миграции
};
// Функция может отсутствовать если миграцию невозможно обратить
export const down: TUmugMigrationFn = async ({
context: { queryInterface, sequelize, transaction },
}) => {
// Описание отката миграции
};
Внутри миграция можно использовать модели:
// src/database/migrations/0001-first-migration.ts
import type { TUmugMigrationFn } from '@/database/types';
import { User } from '@/database/models';
export const up: TUmugMigrationFn = async ({
context: { queryInterface, sequelize, transaction },
}) => {
User.registration(sequelize); // Регистрация модели, так-как транзакции собираются отельный сборкой webpack
await User.create(
{
// Поля необходимые для создания пользователя
},
{ transaction }
);
};
// ...
Также можно пользоваться интерфейсом запросов:
// src/database/migrations/0001-first-migration.ts
import type { TUmugMigrationFn } from '@/database/types';
import { User } from '@/database/models';
export const up: TUmugMigrationFn = async ({
context: { queryInterface, sequelize, transaction },
}) => {
await queryInterface.bulkInsert(
'users',
[
{
login: 'user',
createdAt: new Date(),
updatedAt: new Date(),
},
{
login: 'admin',
createdAt: new Date(),
updatedAt: new Date(),
},
],
{ transaction }
);
};
ВАЖНО: Не забывайте указывать для любых методов общения с БД, что они выполняются в транзакции!!!
4. Контроллеры express
Занимаются обслуживаем конкретных ручек API. Для каждой сущности рекомендуется создавать отдельную папку со свои index.ts см. структуру src/server/controllers/healthchecks.
Пример контроллера пользователя:
// src/server/controllers/users/userDetail.ts
import { Request, Response } from 'express';
import { dbUserController } from '@/database/controllers';
export default function userDetail(req: Request, res: Response) {
const { userId } = req.params;
dbUserController
.getUserById(Number(userId))
.then((user) => {
if (user) {
res.status(200).json(user.toJSON());
} else {
res.status(404).json({
reason: `User with id ${userId} was not found`,
});
}
})
.catch((error) => {
res.status(500).json({ reason: `${error}` });
});
}
Свой index.ts:
// src/server/controllers/users/index.ts
import userDetail from './userDetail';
export default {
userDetail,
};
И подключение в общий index.ts:
// src/server/controllers/index.ts
// ...
export { default as userController } from './users';
5. "Ручки"
Обобщение всех методов работы с конкретным маршрутом.
Пример "ручки" /users:
// src/server/routes/api/users.ts
import { Router } from 'express';
import { userController } from '@/server/controllers';
const router: Router = Router();
router
.route('/users/:userId')
.get([
/* TODO Валидация, подготовка запроса, и т.д. */
userController.userDetail,
])
.path([
/* Обработка частичного обновления */
])
.put([
/* Обработка полного обновления */
])
.delete([
/* Обработка удаления */
]);
router
.route('/users')
.get([
/* Обработка получения всех пользователе */
])
.post([
/* Обработка создания пользователя */
]);
export default router;
Подключение:
// src/server/routes/api/index.ts
import users from './users';
// ...
router.use(checkDBConnection).use(users); // Добавляем свою "ручку"
export default router;