Node.js وMySQL واحدة من المجموعات الأكثر موثوقية واختبارها في المعركة لبناء أنظمة الواجهة الخلفية. يوفر Node.js نموذج إدخال/إخراج غير محظور يحركه الحدث ويتعامل مع الطلبات المتزامنة بكفاءة، بينما يوفر MySQL تكامل البيانات الارتباطية التي تتطلبها تطبيقات الأعمال. إنهم يشكلون معًا أساسًا يدعم كل شيء بدءًا من أفضل الشركات الناشئة وحتى منصات المؤسسات التي تتعامل مع ملايين الطلبات يوميًا.
يتناول هذا الدليل كيفية إنشاء واجهة خلفية على مستوى الإنتاج API، ويغطي هيكل المشروع وتصميم قاعدة البيانات والمصادقة ومعالجة الأخطاء والنشر.
Express.js هيكل المشروع
هيكل المشروع المنظم جيدًا هو أساس الواجهة الخلفية القابلة للصيانة. افصل بين الاهتمامات بوضوح وقم بوضع اتفاقيات في وقت مبكر.
project-root/
src/
config/
database.js
environment.js
logger.js
middleware/
auth.js
errorHandler.js
rateLimiter.js
validator.js
models/
User.js
Product.js
Order.js
index.js
routes/
auth.routes.js
users.routes.js
products.routes.js
orders.routes.js
index.js
services/
auth.service.js
user.service.js
product.service.js
email.service.js
utils/
ApiError.js
asyncHandler.js
pagination.js
app.js
server.js
migrations/
seeders/
tests/
.env
.env.example
package.jsonتقوم نقطة الإدخال بإعداد Express باستخدام البرامج الوسيطة الأساسية:
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const { errorHandler } = require('./middleware/errorHandler');
const routes = require('./routes');
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));
// Request parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Logging
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// API routes
app.use('/api/v1', routes);
// Error handling (must be last)
app.use(errorHandler);
module.exports = app;RESTful API Design
صمم نقاط نهاية API الخاصة بك باتباع اصطلاحات REST. استخدم الأسماء للموارد، وطرق HTTP للإجراءات، وتنسيقات الاستجابة المتسقة.
// src/routes/products.routes.js
const router = require('express').Router();
const { authenticate, authorize } = require('../middleware/auth');
const { validate } = require('../middleware/validator');
const { createProductSchema, updateProductSchema } = require('../validators/product');
const productController = require('../controllers/product.controller');
router.get('/', productController.getAll);
router.get('/:id', productController.getById);
router.post('/',
authenticate,
authorize('admin'),
validate(createProductSchema),
productController.create
);
router.put('/:id',
authenticate,
authorize('admin'),
validate(updateProductSchema),
productController.update
);
router.delete('/:id',
authenticate,
authorize('admin'),
productController.delete
);
module.exports = router;يجب أن تكون وحدات التحكم رفيعة، مما يؤدي إلى تفويض منطق الأعمال إلى فئات الخدمة:
// src/controllers/product.controller.js
const productService = require('../services/product.service');
const { asyncHandler } = require('../utils/asyncHandler');
exports.getAll = asyncHandler(async (req, res) => {
const { page = 1, limit = 20, sort = 'created_at', order = 'DESC', search } = req.query;
const result = await productService.findAll({
page: parseInt(page),
limit: Math.min(parseInt(limit), 100),
sort,
order,
search,
});
res.json({
success: true,
data: result.products,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
totalPages: result.totalPages,
},
});
});
exports.create = asyncHandler(async (req, res) => {
const product = await productService.create(req.body);
res.status(201).json({
success: true,
data: product,
});
});MySQL تجمع الاتصال مع mysql2
يعد تجميع الاتصالات أمرًا بالغ الأهمية للأداء. توفر حزمةmysql2API المستندة إلى الوعد مع البيانات المعدة وتجميع الاتصال خارج الصندوق.
// src/config/database.js
const mysql = require('mysql2/promise');
const logger = require('./logger');
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: parseInt(process.env.DB_POOL_SIZE) || 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 30000,
timezone: '+00:00',
typeCast: function (field, next) {
if (field.type === 'TINY' && field.length === 1) {
return field.string() === '1';
}
return next();
},
});
// Test connection on startup
pool.getConnection()
.then(conn => {
logger.info('MySQL connected successfully');
conn.release();
})
.catch(err => {
logger.error('MySQL connection failed:', err.message);
process.exit(1);
});
module.exports = pool;استخدم دائمًا الاستعلامات ذات المعلمات لمنع حقن SQL:
// NEVER do this
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ALWAYS use parameterized queries
const [rows] = await pool.execute(
'SELECT id, email, first_name, last_name FROM users WHERE email = ?',
[email]
);Sequelize ORM
بالنسبة للتطبيقات ذات علاقات البيانات المعقدة، يوفر Sequelize ORM كامل الميزات مع تعريفات النماذج والارتباطات والترحيلات وبناء الاستعلام.
// src/models/Product.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/sequelize');
const Product = sequelize.define('Product', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
notEmpty: true,
len: [2, 255],
},
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
validate: {
min: 0,
},
},
sku: {
type: DataTypes.STRING(100),
unique: true,
allowNull: false,
},
stock_quantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
validate: {
min: 0,
},
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
}, {
tableName: 'products',
timestamps: true,
underscored: true,
paranoid: true, // Soft deletes
indexes: [
{ fields: ['sku'], unique: true },
{ fields: ['is_active'] },
{ fields: ['price'] },
{ fields: ['created_at'] },
],
});
// Associations
Product.associate = (models) => {
Product.belongsTo(models.Category, { foreignKey: 'category_id' });
Product.hasMany(models.OrderItem, { foreignKey: 'product_id' });
Product.belongsToMany(models.Tag, { through: 'product_tags' });
};
module.exports = Product;المصادقة باستخدام JWT
تنفيذ مصادقة عديمة الحالة باستخدام رموز الويب JSON. استخدم رموز الوصول لطلبات API ورموز التحديث لإدارة الجلسة.
// src/services/auth.service.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { User } = require('../models');
const ApiError = require('../utils/ApiError');
const SALT_ROUNDS = 12;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
exports.register = async ({ email, password, firstName, lastName }) => {
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
throw new ApiError(409, 'Email already registered');
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const user = await User.create({
email,
password: hashedPassword,
first_name: firstName,
last_name: lastName,
});
const tokens = generateTokens(user);
return { user: sanitizeUser(user), ...tokens };
};
exports.login = async ({ email, password }) => {
const user = await User.findOne({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new ApiError(401, 'Invalid email or password');
}
const tokens = generateTokens(user);
return { user: sanitizeUser(user), ...tokens };
};
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRY }
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenType: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
return { accessToken, refreshToken };
}
function sanitizeUser(user) {
const { password, ...userData } = user.toJSON();
return userData;
}تتحقق البرامج الوسيطة للمصادقة من الرموز المميزة على المسارات المحمية:
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const ApiError = require('../utils/ApiError');
exports.authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw new ApiError(401, 'Access token required');
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new ApiError(401, 'Access token expired');
}
throw new ApiError(401, 'Invalid access token');
}
};
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
throw new ApiError(403, 'Insufficient permissions');
}
next();
};
};التحقق من صحة الإدخال باستخدام Joi
التحقق من صحة جميع البيانات الواردة قبل أن تصل إلى منطق عملك. يوفر Joi مكتبة تحقق قوية قائمة على المخطط.
// src/validators/product.js
const Joi = require('joi');
exports.createProductSchema = Joi.object({
name: Joi.string().min(2).max(255).required(),
description: Joi.string().max(5000).optional(),
price: Joi.number().positive().precision(2).required(),
sku: Joi.string().alphanum().max(100).required(),
stock_quantity: Joi.number().integer().min(0).default(0),
category_id: Joi.string().uuid().required(),
tags: Joi.array().items(Joi.string().uuid()).optional(),
is_active: Joi.boolean().default(true),
});
exports.updateProductSchema = Joi.object({
name: Joi.string().min(2).max(255),
description: Joi.string().max(5000).allow(null),
price: Joi.number().positive().precision(2),
stock_quantity: Joi.number().integer().min(0),
category_id: Joi.string().uuid(),
is_active: Joi.boolean(),
}).min(1);
// src/middleware/validator.js
exports.validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
}));
return res.status(400).json({
success: false,
message: 'Validation failed',
errors,
});
}
req.body = value;
next();
};
};معالجة الأخطاء للبرامج الوسيطة
تضمن معالجة الأخطاء المركزية استجابات متسقة للأخطاء وتمنع تسرب المعلومات الحساسة إلى العملاء.
// src/utils/ApiError.js
class ApiError extends Error {
constructor(statusCode, message, errors = []) {
super(message);
this.statusCode = statusCode;
this.errors = errors;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ApiError;
// src/utils/asyncHandler.js
exports.asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// src/middleware/errorHandler.js
const logger = require('../config/logger');
exports.errorHandler = (err, req, res, next) => {
let statusCode = err.statusCode || 500;
let message = err.message || 'Internal Server Error';
// Sequelize validation errors
if (err.name === 'SequelizeValidationError') {
statusCode = 400;
message = 'Validation error';
}
// Sequelize unique constraint
if (err.name === 'SequelizeUniqueConstraintError') {
statusCode = 409;
message = 'Resource already exists';
}
// Log server errors
if (statusCode >= 500) {
logger.error({
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
});
}
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
...(err.errors?.length && { errors: err.errors }),
});
};تحسين الاستعلام وفهرسته
تعد استعلامات قاعدة البيانات الفعالة أمرًا بالغ الأهمية لأداء الواجهة الخلفية. اتبع هذه الاستراتيجيات لإبقاء استعلامات MySQL الخاصة بك سريعة.
- استخدم الفهارس على الأعمدة التي يتم الاستعلام عنها بشكل متكرر- أضف فهارس على الأعمدة المستخدمة في عبارات WHERE وشروط JOIN وعبارات ORDER BY. يجب أن تتبع الفهارس المركبة قاعدة البادئة الموجودة في أقصى اليسار.
- تجنب التحديد *- حدد دائمًا الأعمدة التي تحتاجها. وهذا يقلل من نقل البيانات ويسمح لـ MySQL باستخدام فهارس التغطية.
- استخدم EXPLAIN لتحليل الاستعلامات- قم بتشغيل
EXPLAINقبل استعلاماتك لفهم خطة التنفيذ. ابحث عن عمليات فحص الجدول الكامل وعمليات فرز الملفات والجداول المؤقتة. - تحسين ترقيم الصفحات- بالنسبة لمجموعات البيانات الكبيرة، استخدم ترقيم الصفحات المستند إلى المؤشر (ترقيم الصفحات لمجموعة المفاتيح) بدلاً من OFFSET، الذي يصبح بطيئًا عند ارتفاع أرقام الصفحات.
// Inefficient OFFSET pagination
const [rows] = await pool.execute(
'SELECT * FROM products ORDER BY created_at DESC LIMIT ? OFFSET ?',
[limit, (page - 1) * limit]
);
// Efficient cursor-based pagination
const [rows] = await pool.execute(
`SELECT id, name, price, created_at FROM products
WHERE created_at < ?
ORDER BY created_at DESC
LIMIT ?`,
[cursor, limit]
);عمليات ترحيل قاعدة البيانات
لا تقم مطلقًا بتعديل قواعد بيانات الإنتاج يدويًا. استخدم Sequelize عمليات الترحيل لتغييرات المخطط التي يتم التحكم فيها بالإصدار.
// migrations/20250101000000-create-products-table.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('products', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
},
name: {
type: Sequelize.STRING(255),
allowNull: false,
},
price: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false,
},
sku: {
type: Sequelize.STRING(100),
unique: true,
allowNull: false,
},
category_id: {
type: Sequelize.UUID,
references: {
model: 'categories',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
},
created_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
},
});
await queryInterface.addIndex('products', ['sku']);
await queryInterface.addIndex('products', ['category_id']);
await queryInterface.addIndex('products', ['created_at']);
},
down: async (queryInterface) => {
await queryInterface.dropTable('products');
},
};تحديد المعدل
قم بحماية جهاز API الخاص بك من سوء الاستخدام من خلال تحديد المعدل. استخدمexpress-rate-limitمع متجر Redis لعمليات النشر الموزعة.
// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('../config/redis');
exports.apiLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: {
success: false,
message: 'Too many requests, please try again later',
},
standardHeaders: true,
legacyHeaders: false,
});
exports.authLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000,
max: 5,
message: {
success: false,
message: 'Too many login attempts, please try again later',
},
skipSuccessfulRequests: true,
});التسجيل باستخدام Winston
تحتاج تطبيقات الإنتاج إلى تسجيل منظم باستخدام عمليات نقل ومستويات تسجيل متعددة.
// src/config/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'api-server' },
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880,
maxFiles: 10,
}),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
module.exports = logger;Docker النشر
قم بتخزين تطبيقك من أجل عمليات نشر متسقة عبر البيئات.
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
COPY migrations/ ./migrations/
COPY package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=mysql
- DB_USER=app_user
- DB_PASSWORD_FILE=/run/secrets/db_password
- DB_NAME=myapp
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: myapp
MYSQL_USER: app_user
MYSQL_PASSWORD_FILE: /run/secrets/db_password
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mysql_data:الاستنتاج
يتطلب إنشاء واجهة خلفية قوية لـ Node.js وMySQL الاهتمام بالهندسة المعمارية والأمن والأداء والمخاوف التشغيلية. من خلال إنشاء بنية مشروع نظيفة، وتنفيذ المصادقة والتحقق المناسبين، وتحسين استعلامات قاعدة البيانات، ووضع النشر في حاوية، يمكنك إنشاء واجهة خلفية آمنة وفعالة وقابلة للصيانة. ابدأ بالأساسيات الموضحة في هذا الدليل، وقم بقياس أداء التطبيق الخاص بك في ظل التحميل الواقعي، ثم قم بالتكرار على الاختناقات التي تكتشفها. لقد تم إثبات الأنماط المعروضة هنا عبر الآلاف من تطبيقات الإنتاج وستكون بمثابة أساس متين لأنظمة الواجهة الخلفية لديك.