ساختار بندی پروژه بکند

در این بخش مرحله به مرحله توسعه بکند پروژه را شرح میدهم.

مراحل توسعه بک‌اند

در این قسمت به‌صورت کامل مراحل توسعه بک‌اند با ساختار پروژه مورد نظر شرکت ماورا توضیح داده می‌شود.

فهرست مراحل

Screenshot 2025-05-14 094931
  1. ایجاد Repository در گیت‌لب شرکت
    - ورود به https://git.mavaratech.com و لاگین با حساب کاربری شرکت
    - کلیک روی New Project و انتخاب Create blank project
    - وارد کردن نام، توضیحات و تنظیم سطح دسترسی
  2. تنظیم کانفیگ پروژه بر اساس ساختار تعریف‌شده
    - ایجاد پوشه config/ و فایل‌های default.json و production.json
    - ست کردن پارامترهایی مثل database.url، baseUrl و تنظیمات SSO
    - بارگذاری متغیرهای محیطی (Environment Variables)
  3. طراحی و پیاده‌سازی دیتابیس
    - مدل‌سازی ER-Diagram و تعریف جداول، ستون‌ها، PK و FK
    - نوشتن اسکریپت‌های DDL و اجرای آن‌ها در محیط توسعه
    - اضافه کردن seed data برای جداول پایه مثل کاربران و نقش‌ها
  4. ایجاد Routeها، API و کنترلرها
    - تعریف نقشه مسیرها (routes) در src/routes
    - پیاده‌سازی کنترلرها (Controllers) برای هر endpoint در src/controllers
    - مستندسازی APIها با OpenAPI/Swagger
  5. اطلاع‌رسانی به PMO درباره اتمام پروژه
    - ارسال ایمیل یا تیکت در سیستم PMO مبنی بر پایان کار
    - ارائه گزارش نهایی و لینک به مخزن کد و مستندات تکمیلی

ساخت Repository

مراحل گام‌به‌گام «ساخت Repository» در گیت‌لب شرکت

Screenshot 2025-05-14 095714
  1. ورود به گیت‌لب
    ابتدا مرورگر خود را باز کنید و به آدرس زیر مراجعه کنید:
    https://git.mavaratech.com/
    سپس با حساب کاربری شرکت (نام کاربری و رمز عبور) وارد شوید. Screenshot 2025-05-13 110843
  2. ایجاد پروژه جدید Screenshot 2025-05-13 111042
    • پس از ورود، از منوی سمت چپ یا بالای صفحه روی دکمه‌ی New project کلیک کنید.
    • گزینه‌ی Create blank project را انتخاب کنید.
  3. وارد کردن اطلاعات پروژه
    • Project name: نام پروژه را به صورت انگلیسی و بدون فاصله (مثلاً my-awesome-app) وارد کنید.
    • Project slug: معمولاً خودکار پر می‌شود اما در صورت نیاز ویرایش کنید.
    • Project description (اختیاری): توضیح کوتاهی درباره‌ی هدف پروژه بنویسید.
    • Visibility Level: سطح دسترسی پروژه را مشخص کنید (Private، Internal یا Public).
  4. ساخت ریپازیتوری
    پس از تکمیل اطلاعات، روی دکمه‌ی Create project کلیک کنید. گیت‌لب به طور خودکار مخزن خالی را برای شما ایجاد کرده و وارد صفحه‌ی پروژه می‌شوید.
  5. پیکربندی اولیه (اختیاری)
    • در صورت نیاز، یک فایل README.md، .gitignore یا لایسنس (LICENSE) اضافه کنید.
    • با فعال کردن Initialize repository with a README یک فایل README به مخزن اضافه می‌شود.

اکنون ریپازیتوری شما آماده‌ی کار است و می‌توانید آن را کلون کرده، کدها را پوش کنید و توسعه را ادامه دهید.

ساختار بندی پروژه

مقدمه

در این راهنما، گام‌به‌گام نحوهٔ شروع یک پروژه فول‌استک با React و Node.js و ساختار پیشنهادی پوشه‌ها و فایل‌ها تشریح شده است. این مستند مناسب تیم‌های توسعه است تا استاندارد یکپارچه‌ای برای ایجاد پروژه‌های جدید داشته باشند.

Screenshot 2025-05-14 095936

راه‌اندازی پروژه بک‌اند (Node.js + Express)

mkdir backend
cd backend
npm init -y
npm install express dotenv sequelize jsonwebtoken bcryptjs
npm install --save-dev nodemon

ساختار پوشه‌ها

backend/
├── config/
│   ├── db.js
│   ├── config.js
│   ├── development.js
│   ├── production.js
│   └── .dev.js
├── controller/
│   └── userController.js
├── routes/
│   └── userRoutes.js
├── service/
│   └── userService.js
├── utils/
│   └── generateToken.js
├── auth/
│   └── authMiddleware.js
├── .env
├── server.js
└── package.json

در server.js حتماً باید پورت پروژه را با مسئول DevOps تیم هماهنگ کنید.

پیکربندی اتصال به دیتابیس (config)

فایل db.js

این کلاس مسئول ایجاد اتصال Sequelize به PostgreSQL است:

import { Sequelize } from 'sequelize';
import configs from './config.js';

class DBConnection { constructor() { this.db_url = configs.db.url; const options = { dialect: "postgres", timezone: "+03:30", schema: 'schema', }; if (configs.db.useSSL === "true") { options.dialectOptions = { ssl: { require: true, rejectUnauthorized: false } }; } this.db = new Sequelize(this.db_url, options); }

getSequelizeInstance() { return this.db; } }

const sequelize = new DBConnection().getSequelizeInstance(); export default sequelize;

فایل config.js

براساس مقدار NODE_ENV یکی از فایل‌های محیطی (development، production، .dev) وارد می‌شود:

const env = process.env.NODE_ENV || 'development';
const configs = await import(`./.${env}.js`).then(m => m.default);
export default configs;

پیکربندی اتصال به دیتابیس (config)

فایل db.js

این کلاس مسئول ایجاد اتصال Sequelize به PostgreSQL است:


import { Sequelize } from 'sequelize';
import configs from './config.js';

class DBConnection { constructor() { this.db_url = configs.db.url; const options = { dialect: "postgres", timezone: "+03:30", schema: 'schema Name', }; if (configs.db.useSSL === "true") { options.dialectOptions = { ssl: { require: true, rejectUnauthorized: false } }; } this.db = new Sequelize(this.db_url, options); }

getSequelizeInstance() { return this.db; } }

const sequelize = new DBConnection().getSequelizeInstance(); export default sequelize;

فایل config.js

براساس مقدار NODE_ENV یکی از فایل‌های محیطی (development، production، .dev) وارد می‌شود:


const env = process.env.NODE_ENV || 'development';
const configs = await import(`./.${env}.js`).then(m => m.default);
export default configs;
    

فایل‌های محیطی

در این فایل‌ها تنظیمات مربوط به هر محیط (production، development، .dev) قرار دارد:

production.js


export default {
  app: {
    port: 3000,
    apiBaseUrl: process.env.API_BASE_URL || 'url',
    ssoBaseUrl: 'url',
    paymentBaseUrl: process.env.PAYMENT_BASE_URL || 'url',
    useMockUsername: process.env.USE_MOCK_USERNAME || 'false',
    mockUsername: process.env.MOCK_USERNAME || '',
  },
  db: {
    url: process.env.DB_URL || 'postgresql://postgreess',
    useSSL: process.env.DB_USE_SSL || 'false',
  },
  redis: {
    host: process.env.REDIS_HOST || 'host url',
    port: process.env.REDIS_PORT || port,
    sentinelNodes: process.env.REDIS_SENTINEL_NODES || '',
    sentinelName: process.env.REDIS_SENTINEL_MASTER || 'mymaster',
    password: process.env.REDIS_PASSWORD || 'password',
    dbNo: process.env.REDIS_DB_NO || 0,
    prefix: process.env.REDIS_PREFIX || 'integropia:'
  }
};
  

development.js


export default {
  app: {
    port: 3001,
    apiBaseUrl: 'BaseUrl that you recive',
    useMockUsername: 'true',
    mockUsername: process.env.MOCK_USERNAME || '',
  },
  db: {
    url: process.env.DB_URL || 'postgresql:postgressName',
    useSSL: 'false',
  },
  redis: {
    host: process.env.REDIS_HOST || '87.248.137.22',
    port: process.env.REDIS_PORT || 6379,
    sentinelNodes: process.env.REDIS_SENTINEL_NODES || '',
    sentinelName: process.env.REDIS_SENTINEL_MASTER || 'mymaster',
    password: process.env.REDIS_PASSWORD || 'password',
    dbNo: process.env.REDIS_DB_NO || 0,
    prefix: process.env.REDIS_PREFIX || 'integropia:',
  },
};
  

.dev.js


export default {
  app: {
    port: 3000,
    apiBaseUrl: process.env.API_BASE_URL || 'url',
    ssoBaseUrl: 'url,
    paymentBaseUrl: process.env.PAYMENT_BASE_URL || 'url',
    useMockUsername: process.env.USE_MOCK_USERNAME || 'false',
    mockUsername: process.env.MOCK_USERNAME || '',
  },
  db: {
    url: process.env.DB_URL || 'postgresql:postgress',
    useSSL: process.env.DB_USE_SSL || 'false',
  },
  redis: {
    host: process.env.REDIS_HOST || 'host',
    port: process.env.REDIS_PORT || port,
    sentinelNodes: process.env.REDIS_SENTINEL_NODES || '',
    sentinelName: process.env.REDIS_SENTINEL_MASTER || 'mymaster',
    password: process.env.REDIS_PASSWORD || 'password',
    dbNo: process.env.REDIS_DB_NO || 0,
    prefix: process.env.REDIS_PREFIX || 'integropia:'
  }
};
  

نکته بسیار مهم: در این بخش باید secret_key مینی‌اپ فرانت خود را در فایل‌های production.js یا development.js قرار دهید و نام مینی‌اپ در تمام پیکربندی‌ها یکسان باشد.

طراحی ساختار دیتابیس (ER-Diagram)

در ادامه یک مثال از بخش طراحی دیتابیس آورده شده است

Screenshot 2025-05-14 102836
  1. طراحی ساختار دیتابیس (ER-Diagram)
    - مشخص کردن موجودیت‌ها (Entities): User، Request و RequestStatus
    - تعریف روابط: هر User می‌تواند چند Request داشته باشد؛ هر Request چند RequestStatus
    - تعیین کلیدهای اصلی (PK) و خارجی (FK) در هر جدول
  2. تنظیم کانفیگ Sequelize برای PostgreSQL
    - نصب پکیج‌ها:
    npm install sequelize pg pg-hstore
    - ایجاد فایل کانفیگ src/configs/database.js:
    import { Sequelize } from 'sequelize';
    const sequelize = new Sequelize(
      process.env.DB_NAME,
      process.env.DB_USER,
      process.env.DB_PASS,
      {
        host: process.env.DB_HOST,
        dialect: 'postgres',
        logging: false,
      }
    );
    

    export default sequelize;

  3. ایجاد مدل User
    - تعریف فیلدهای پایه مثل userId، name، email و role
    import { DataTypes } from 'sequelize';
    import sequelize from '../configs/database.js';
    

    const User = sequelize.define('User', { userId: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, name: { type: DataTypes.STRING(100), allowNull: false, }, email: { type: DataTypes.STRING(150), allowNull: false, unique: true, }, role: { type: DataTypes.ENUM('user', 'expert', 'admin'), defaultValue: 'user', }, }, { tableName: 'users', timestamps: true, });

    export default User;

  4. ایجاد مدل Request
    - فیلدهای requestId، userId، expertId، text و trackingCode
    import { DataTypes } from 'sequelize';
    import sequelize from '../configs/database.js';
    import User from './user.js';
    import { v4 as uuidv4 } from 'uuid';
    

    const Request = sequelize.define('Request', { requestId: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, userId: { type: DataTypes.INTEGER, references: { model: User, key: 'userId' }, allowNull: true, }, expertId: { type: DataTypes.INTEGER, references: { model: User, key: 'userId' }, allowNull: true, }, text: { type: DataTypes.TEXT, allowNull: false, }, trackingCode: { type: DataTypes.STRING(20), allowNull: false, unique: true, defaultValue: () => uuidv4().slice(0, 20), }, }, { tableName: 'requests', timestamps: true, });

    export default Request;

  5. ایجاد مدل RequestStatus
    - ENUM وضعیت‌ها، ارجاع به requestId و نگهداری previous_state
    import { DataTypes } from 'sequelize';
    import sequelize from '../configs/database.js';
    import Request from './request.js';
    

    const RequestStatus = sequelize.define('RequestStatus', { statusId: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, requestId: { type: DataTypes.INTEGER, references: { model: Request, key: 'requestId' }, allowNull: false, }, statusType: { type: DataTypes.ENUM('started','pending','accepted','rejected','done'), defaultValue: 'started', }, expertComment: { type: DataTypes.TEXT, defaultValue: '', }, userComment: { type: DataTypes.TEXT, defaultValue: '', }, files: { type: DataTypes.JSONB, allowNull: true, }, previous_state: { type: DataTypes.INTEGER, references: { model: 'request_statuses', key: 'statusId' }, allowNull: true, }, }, { tableName: 'request_statuses', timestamps: true, hooks: { beforeCreate: async (status) => { const last = await RequestStatus.findOne({ where: { requestId: status.requestId }, order: [['statusId','DESC']], }); if (last) status.previous_state = last.statusId; } } }); export default RequestStatus;

  6. تنظیم ارتباطات (Associations)
    - در فایل src/models/index.js یا انتهای هر مدل:
    import User from './user.js';
    import Request from './request.js';
    import RequestStatus from './requestStatus.js';
    // User ↔ Request
    User.hasMany(Request, { foreignKey: 'userId', as: 'Requests' });
    Request.belongsTo(User, { foreignKey: 'userId', as: 'Customer' });
    User.hasMany(Request, { foreignKey: 'expertId', as: 'ExpertRequests' });
    Request.belongsTo(User, { foreignKey: 'expertId', as: 'Expert' });
    // Request ↔ RequestStatus
    Request.hasMany(RequestStatus, { foreignKey: 'requestId', as: 'Statuses' });
    RequestStatus.belongsTo(Request, { foreignKey: 'requestId', as: 'Request' });
    export { User, Request, RequestStatus };
  7. پیاده‌سازی الگوی وضعیت (State Pattern)
    - ساخت پوشه src/models/state/ و ایجاد کلاس‌های وضعیت:
    import { handleReqRejected } from '../../utils/status.utils.js';
    class AcceptedState {
    constructor(context) { this.context = context; }
    async transitionTo(statusType) {
    if (!statusType) throw new Error('Invalid statusType');
    if (statusType === 'rejected') {
    return await handleReqRejected(this.context);
    }
    throw new Error(`Cannot transition from accepted to ${statusType}`);
    }
    }
    

    export default AcceptedState;

    - در سرویس Request فراخوانی وضعیت:
    import AcceptedState from './state/acceptedState.js';
    import PendingState from './state/pendingState.js';
    // …
    class RequestService {
    constructor(request) {
    this.request = request;
    this.state = this._getStateInstance(request.currentStatus);
    }
    _getStateInstance(type) {
    switch(type) {
    case 'accepted': return new AcceptedState(this.request);
    // …
    }
    }
    async changeStatus(toType) {
    return this.state.transitionTo(toType);
    }
    }

نوشتن APi, Route , Controller

در ادامه یک مثال از طراحی route , controller اورده شده است

Screenshot 2025-05-14 104259
  1. ایجاد ساختار پوشه‌ها برای Route و Controller
    - در ریشه پروژه پوشه‌های src/routes/ و src/controllers/ را بسازید
    - هر موجودیت (مثلاً requests و users) یک فایل Route و یک فایل Controller دارد
    - ساختار نهایی:
    src/
    ├─ controllers/
    │  ├─ requestController.js
    │  └─ userController.js
    └─ routes/
       ├─ requestRoutes.js
       └─ userRoutes.js
    
  2. تعریف Routeها با استفاده از Express
    - در src/routes/requestRoutes.js مسیرهای CRUD را تعریف کنید:
    import { Router } from 'express';
    import {
      createRequest,
      getAllRequests,
      getRequestById,
      updateRequest,
      deleteRequest
    } from '../controllers/requestController.js';
    

    const router = Router();

    router.post('/', createRequest); router.get('/', getAllRequests); router.get('/:id', getRequestById); router.put('/:id', updateRequest); router.delete('/:id', deleteRequest);

    export default router;

  3. ایجاد Controllerها با منطق کاری
    - در src/controllers/requestController.js توابع CRUD را پیاده کنید:
    import Request from '../models/request.js';
    

    export const createRequest = async (req, res, next) => { try { const newReq = await Request.create({ userId: req.body.userId, expertId: req.body.expertId, text: req.body.text }); res.status(201).json(newReq); } catch (err) { next(err); } };

    export const getAllRequests = async (_req, res, next) => { try { const list = await Request.findAll(); res.json(list); } catch (err) { next(err); } };

    export const getRequestById = async (req, res, next) => { try { const item = await Request.findByPk(req.params.id); if (!item) return res.status(404).json({ message: 'Not found' }); res.json(item); } catch (err) { next(err); } };

    export const updateRequest = async (req, res, next) => { try { const [updated] = await Request.update(req.body, { where: { requestId: req.params.id } }); if (!updated) return res.status(404).json({ message: 'Not found' }); res.json({ message: 'Updated' }); } catch (err) { next(err); } };

    export const deleteRequest = async (req, res, next) => { try { const deleted = await Request.destroy({ where: { requestId: req.params.id } }); if (!deleted) return res.status(404).json({ message: 'Not found' }); res.status(204).end(); } catch (err) { next(err); } };

  4. وصل کردن Routeها به اپلیکیشن اصلی
    - در src/app.js یا index.js این کار را انجام دهید:
    import express from 'express';
    import bodyParser from 'body-parser';
    import requestRoutes from './routes/requestRoutes.js';
    import userRoutes    from './routes/userRoutes.js';
    

    const app = express(); app.use(bodyParser.json());

    app.use('/api/requests', requestRoutes); app.use('/api/users', userRoutes);

    // هندلینگ خطا app.use((err, _req, res, _next) => { console.error(err); res.status(500).json({ error: err.message }); });

    export default app;

نکات مهم پایانی

Screenshot 2025-05-14 094752

نکات مهم