Node.js

مشروع Node.js متكامل — API مع مصادقة JWT 2026

📅 2026-11-10⏱ 13 دقائق قراءة
🎉 **مبروك! وصلت إلى المقال الأخير في مسار Node.js!** لقد تعلمت كل الأساسيات: HTTP Server، Express، Routing، Middleware، REST API، و MongoDB. الآن حان الوقت لتطبيق كل هذه المفاهيم في **مشروع حقيقي متكامل**. في هذا الدليل العملي، سنبني **API متكامل** مع **مصادقة JWT** — وهو ما يبنيه المطورون المحترفون فعلاً. ## ما سنبنيه **API مدونة** يحتوي على: - ✅ **تسجيل المستخدمين** (Register). - ✅ **تسجيل الدخول** (Login). - ✅ **مصادقة JWT** (Authentication). - ✅ **إدارة المستخدمين** (CRUD). - ✅ **إدارة المقالات** (CRUD). - ✅ **العلاقات** (User → Posts). - ✅ **حماية المسارات** (Protected Routes). - ✅ **رفع الصور**. - ✅ **معالجة الأخطاء**. - ✅ **نشر المشروع**. ## هيكل المشروع ``` blog-api/ ├── src/ │ ├── config/ │ │ └── database.js │ ├── controllers/ │ │ ├── authController.js │ │ ├── userController.js │ │ └── postController.js │ ├── middleware/ │ │ ├── auth.js │ │ ├── errorHandler.js │ │ ├── upload.js │ │ └── validate.js │ ├── models/ │ │ ├── User.js │ │ └── Post.js │ ├── routes/ │ │ ├── authRoutes.js │ │ ├── userRoutes.js │ │ └── postRoutes.js │ ├── utils/ │ │ ├── ApiError.js │ │ ├── ApiResponse.js │ │ └── generateToken.js │ └── app.js ├── uploads/ ├── .env ├── .gitignore ├── server.js └── package.json ``` ## الخطوة 1: إعداد المشروع ```bash mkdir blog-api cd blog-api npm init -y npm install express mongoose bcryptjs jsonwebtoken dotenv cors multer helmet morgan express-rate-limit npm install -D nodemon ``` **المكتبات:** <table> <thead> <tr> <th>المكتبة</th> <th>الوظيفة</th> </tr> </thead> <tbody> <tr> <td><code>express</code></td> <td>إطار العمل</td> </tr> <tr> <td><code>mongoose</code></td> <td>MongoDB</td> </tr> <tr> <td><code>bcryptjs</code></td> <td>تشفير كلمات المرور</td> </tr> <tr> <td><code>jsonwebtoken</code></td> <td>JWT</td> </tr> <tr> <td><code>dotenv</code></td> <td>متغيرات البيئة</td> </tr> <tr> <td><code>cors</code></td> <td>السماح بالطلبات</td> </tr> <tr> <td><code>multer</code></td> <td>رفع الملفات</td> </tr> <tr> <td><code>helmet</code></td> <td>حماية الرؤوس</td> </tr> <tr> <td><code>morgan</code></td> <td>تسجيل الطلبات</td> </tr> <tr> <td><code>express-rate-limit</code></td> <td>حد الطلبات</td> </tr> </tbody> </table> ## الخطوة 2: `.env` ``` PORT=3000 NODE_ENV=development MONGODB_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/blog-api JWT_SECRET=your-super-secret-key-change-me JWT_EXPIRE=7d ``` ## الخطوة 3: `.gitignore` ``` node_modules/ .env uploads/ *.log .DS_Store dist/ coverage/ ``` ## الخطوة 4: `src/utils/ApiError.js` ```javascript class ApiError extends Error { constructor(statusCode, message) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } module.exports = ApiError; ``` ## الخطوة 5: `src/utils/ApiResponse.js` ```javascript class ApiResponse { constructor(statusCode, data, message = "Success") { this.statusCode = statusCode; this.data = data; this.message = message; this.success = statusCode < 400; } } module.exports = ApiResponse; ``` ## الخطوة 6: `src/utils/generateToken.js` ```javascript const jwt = require("jsonwebtoken"); function generateToken(userId) { return jwt.sign( { id: userId }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRE || "7d" } ); } module.exports = generateToken; ``` ## الخطوة 7: `src/config/database.js` ```javascript const mongoose = require("mongoose"); async function connectDB() { try { const conn = await mongoose.connect(process.env.MONGODB_URI); console.log(`✅ MongoDB: ${conn.connection.host}`); } catch (error) { console.error(`❌ خطأ: ${error.message}`); process.exit(1); } } module.exports = connectDB; ``` ## الخطوة 8: `src/models/User.js` ```javascript const mongoose = require("mongoose"); const bcrypt = require("bcryptjs"); const userSchema = new mongoose.Schema( { name: { type: String, required: [true, "الاسم مطلوب"], trim: true, minlength: [2, "الاسم قصير جداً"], maxlength: [50, "الاسم طويل جداً"], }, email: { type: String, required: [true, "البريد مطلوب"], unique: true, lowercase: true, trim: true, match: [/^\S+@\S+\.\S+$/, "البريد غير صحيح"], }, password: { type: String, required: [true, "كلمة المرور مطلوبة"], minlength: [6, "كلمة المرور قصيرة"], select: false, // ← لا تُرجع تلقائياً }, role: { type: String, enum: ["user", "admin"], default: "user", }, avatar: String, }, { timestamps: true } ); // تشفير كلمة المرور قبل الحفظ userSchema.pre("save", async function (next) { if (!this.isModified("password")) return next(); this.password = await bcrypt.hash(this.password, 12); next(); }); // مقارنة كلمة المرور userSchema.methods.comparePassword = async function (candidate) { return bcrypt.compare(candidate, this.password); }; module.exports = mongoose.model("User", userSchema); ``` ## الخطوة 9: `src/models/Post.js` ```javascript const mongoose = require("mongoose"); const postSchema = new mongoose.Schema( { title: { type: String, required: [true, "العنوان مطلوب"], trim: true, minlength: [5, "العنوان قصير"], maxlength: [200, "العنوان طويل"], }, content: { type: String, required: [true, "المحتوى مطلوب"], minlength: [20, "المحتوى قصير"], }, image: String, tags: [String], author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, }, published: { type: Boolean, default: false, }, }, { timestamps: true } ); module.exports = mongoose.model("Post", postSchema); ``` ## الخطوة 10: `src/middleware/auth.js` ```javascript const jwt = require("jsonwebtoken"); const User = require("../models/User"); const ApiError = require("../utils/ApiError"); async function protect(req, res, next) { try { let token; if ( req.headers.authorization && req.headers.authorization.startsWith("Bearer") ) { token = req.headers.authorization.split(" ")[1]; } if (!token) { return next(new ApiError(401, "غير مصرح — توكن مفقود")); } const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await User.findById(decoded.id); if (!user) { return next(new ApiError(401, "المستخدم غير موجود")); } req.user = user; next(); } catch (error) { next(new ApiError(401, "توكن غير صالح")); } } // صلاحيات محددة function authorize(...roles) { return (req, res, next) => { if (!roles.includes(req.user.role)) { return next(new ApiError(403, "غير مصرح بهذا الإجراء")); } next(); }; } module.exports = { protect, authorize }; ``` ## الخطوة 11: `src/middleware/errorHandler.js` ```javascript const ApiError = require("../utils/ApiError"); const notFound = (req, res, next) => { next(new ApiError(404, `المسار غير موجود: ${req.originalUrl}`)); }; const errorHandler = (err, req, res, next) => { let error = { ...err }; error.message = err.message; // Mongoose: ID خاطئ if (err.name === "CastError") { error = new ApiError(400, "معرف غير صالح"); } // Mongoose: خطأ تحقق if (err.name === "ValidationError") { const messages = Object.values(err.errors).map((e) => e.message); error = new ApiError(400, messages.join(", ")); } // Mongoose: تكرار if (err.code === 11000) { const field = Object.keys(err.keyValue)[0]; error = new ApiError(400, `${field} مستخدم بالفعل`); } // JWT if (err.name === "JsonWebTokenError") { error = new ApiError(401, "توكن غير صالح"); } if (err.name === "TokenExpiredError") { error = new ApiError(401, "توكن منتهي الصلاحية"); } const statusCode = error.statusCode || 500; const message = error.message || "خطأ في السيرفر"; if (process.env.NODE_ENV === "development") { console.error("❌", err); } res.status(statusCode).json({ success: false, statusCode, message, ...(process.env.NODE_ENV === "development" && { stack: err.stack }), }); }; module.exports = { notFound, errorHandler }; ``` ## الخطوة 12: `src/middleware/upload.js` ```javascript const multer = require("multer"); const path = require("path"); const ApiError = require("../utils/ApiError"); const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "uploads/"); }, filename: (req, file, cb) => { const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${path.extname(file.originalname)}`; cb(null, uniqueName); }, }); const fileFilter = (req, file, cb) => { const allowed = /jpeg|jpg|png|webp/; const extname = allowed.test(path.extname(file.originalname).toLowerCase()); const mimetype = allowed.test(file.mimetype); if (extname && mimetype) { cb(null, true); } else { cb(new ApiError(400, "يُسمح بالصور فقط")); } }; const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB }); module.exports = upload; ``` ## الخطوة 13: `src/controllers/authController.js` ```javascript const User = require("../models/User"); const ApiError = require("../utils/ApiError"); const ApiResponse = require("../utils/ApiResponse"); const generateToken = require("../utils/generateToken"); // التسجيل exports.register = async (req, res, next) => { try { const { name, email, password } = req.body; // التحقق من وجود المستخدم const existingUser = await User.findOne({ email }); if (existingUser) { return next(new ApiError(400, "البريد مستخدم بالفعل")); } // إنشاء المستخدم const user = await User.create({ name, email, password }); // إنشاء التوكن const token = generateToken(user._id); res.status(201).json( new ApiResponse( 201, { user: { id: user._id, name: user.name, email: user.email }, token, }, "تم التسجيل بنجاح" ) ); } catch (error) { next(error); } }; // تسجيل الدخول exports.login = async (req, res, next) => { try { const { email, password } = req.body; // جلب المستخدم مع كلمة المرور const user = await User.findOne({ email }).select("+password"); if (!user) { return next(new ApiError(401, "بيانات الدخول غير صحيحة")); } // التحقق من كلمة المرور const isMatch = await user.comparePassword(password); if (!isMatch) { return next(new ApiError(401, "بيانات الدخول غير صحيحة")); } // إنشاء التوكن const token = generateToken(user._id); res.json( new ApiResponse( 200, { user: { id: user._id, name: user.name, email: user.email }, token, }, "تم الدخول بنجاح" ) ); } catch (error) { next(error); } }; // معلوماتي exports.getMe = async (req, res, next) => { try { const user = await User.findById(req.user._id); res.json(new ApiResponse(200, user)); } catch (error) { next(error); } }; ``` ## الخطوة 14: `src/controllers/postController.js` ```javascript const Post = require("../models/Post"); const ApiError = require("../utils/ApiError"); const ApiResponse = require("../utils/ApiResponse"); // جلب كل المقالات (مع Pagination) exports.getAllPosts = async (req, res, next) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const skip = (page - 1) * limit; const posts = await Post.find({ published: true }) .populate("author", "name email avatar") .sort("-createdAt") .skip(skip) .limit(limit); const total = await Post.countDocuments({ published: true }); res.json( new ApiResponse(200, { posts, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }) ); } catch (error) { next(error); } }; // جلب مقال واحد exports.getPostById = async (req, res, next) => { try { const post = await Post.findById(req.params.id).populate( "author", "name email avatar" ); if (!post) return next(new ApiError(404, "المقال غير موجود")); res.json(new ApiResponse(200, post)); } catch (error) { next(error); } }; // إنشاء مقال (محمي) exports.createPost = async (req, res, next) => { try { const post = await Post.create({ ...req.body, author: req.user._id, image: req.file?.filename, }); await post.populate("author", "name email"); res.status(201).json(new ApiResponse(201, post, "تم الإنشاء")); } catch (error) { next(error); } }; // تحديث مقال (محمي) exports.updatePost = async (req, res, next) => { try { let post = await Post.findById(req.params.id); if (!post) return next(new ApiError(404, "المقال غير موجود")); // التحقق من الملكية if (post.author.toString() !== req.user._id.toString() && req.user.role !== "admin") { return next(new ApiError(403, "غير مصرح")); } post = await Post.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true, }); res.json(new ApiResponse(200, post, "تم التحديث")); } catch (error) { next(error); } }; // حذف مقال (محمي) exports.deletePost = async (req, res, next) => { try { const post = await Post.findById(req.params.id); if (!post) return next(new ApiError(404, "المقال غير موجود")); if (post.author.toString() !== req.user._id.toString() && req.user.role !== "admin") { return next(new ApiError(403, "غير مصرح")); } await post.deleteOne(); res.json(new ApiResponse(200, null, "تم الحذف")); } catch (error) { next(error); } }; ``` ## الخطوة 15: `src/routes/authRoutes.js` ```javascript const express = require("express"); const router = express.Router(); const authController = require("../controllers/authController"); const { protect } = require("../middleware/auth"); router.post("/register", authController.register); router.post("/login", authController.login); router.get("/me", protect, authController.getMe); module.exports = router; ``` ## الخطوة 16: `src/routes/postRoutes.js` ```javascript const express = require("express"); const router = express.Router(); const postController = require("../controllers/postController"); const { protect } = require("../middleware/auth"); const upload = require("../middleware/upload"); router.get("/", postController.getAllPosts); router.get("/:id", postController.getPostById); router.post("/", protect, upload.single("image"), postController.createPost); router.put("/:id", protect, postController.updatePost); router.delete("/:id", protect, postController.deletePost); module.exports = router; ``` ## الخطوة 17: `src/app.js` ```javascript const express = require("express"); const cors = require("cors"); const helmet = require("helmet"); const morgan = require("morgan"); const rateLimit = require("express-rate-limit"); const { notFound, errorHandler } = require("./middleware/errorHandler"); const app = express(); // Security app.use(helmet()); app.use(cors()); // Body Parser app.use(express.json({ limit: "10mb" })); app.use(express.urlencoded({ extended: true })); // Logger if (process.env.NODE_ENV === "development") { app.use(morgan("dev")); } // Rate Limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: "تم تجاوز الحد، حاول لاحقاً", }); app.use("/api", limiter); // Static Files app.use("/uploads", express.static("uploads")); // Health Check app.get("/api/health", (req, res) => { res.json({ status: "OK", timestamp: new Date().toISOString(), uptime: process.uptime(), }); }); // Routes app.use("/api/auth", require("./routes/authRoutes")); app.use("/api/posts", require("./routes/postRoutes")); app.use("/api/users", require("./routes/userRoutes")); // Error Handling app.use(notFound); app.use(errorHandler); module.exports = app; ``` ## الخطوة 18: `server.js` ```javascript require("dotenv").config(); const connectDB = require("./src/config/database"); const app = require("./src/app"); const PORT = process.env.PORT || 3000; connectDB().then(() => { app.listen(PORT, () => { console.log(`🚀 السيرفر على http://localhost:${PORT}`); console.log(`📋 Health: http://localhost:${PORT}/api/health`); }); }); ``` ## الخطوة 19: تشغيل المشروع ```bash npm run dev ``` ## اختبار API ### 1. التسجيل ```bash curl -X POST http://localhost:3000/api/auth/register \ -H "Content-Type: application/json" \ -d '{"name":"أحمد","email":"ahmed@example.com","password":"123456"}' ``` **النتيجة:** ```json { "success": true, "data": { "user": { "id": "...", "name": "أحمد", "email": "ahmed@example.com" }, "token": "eyJhbGciOiJIUzI1NiIs..." }, "message": "تم التسجيل بنجاح" } ``` ### 2. تسجيل الدخول ```bash curl -X POST http://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"ahmed@example.com","password":"123456"}' ``` ### 3. إنشاء مقال (مع التوكن) ```bash curl -X POST http://localhost:3000/api/posts \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"title":"مقالي الأول","content":"هذا محتوى المقال...","published":true}' ``` ### 4. جلب المقالات ```bash curl http://localhost:3000/api/posts ``` ## تمارين إضافية ### تمرين 1: التحقق من البريد أضف خطوة تأكيد البريد الإلكتروني. **الحل:** ```javascript const crypto = require("crypto"); // في User Schema emailVerificationToken: String, emailVerified: { type: Boolean, default: false }, // في authController const token = crypto.randomBytes(20).toString("hex"); user.emailVerificationToken = token; // أرسل البريد مع الرابط ``` ### تمرين 2: إعادة تعيين كلمة المرور أضف ميزة "نسيت كلمة المرور". **الحل:** ```javascript exports.forgotPassword = async (req, res, next) => { const user = await User.findOne({ email: req.body.email }); if (!user) return next(new ApiError(404, "المستخدم غير موجود")); const resetToken = crypto.randomBytes(20).toString("hex"); user.resetPasswordToken = crypto .createHash("sha256") .update(resetToken) .digest("hex"); user.resetPasswordExpire = Date.now() + 10 * 60 * 1000; await user.save(); // أرسل البريد res.json(new ApiResponse(200, { resetToken }, "تم إرسال الرابط")); }; ``` ### تمرين 3: التعليقات أضف نظام تعليقات للمقالات. **الحل:** ```javascript const commentSchema = new mongoose.Schema( { text: { type: String, required: true }, author: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, post: { type: mongoose.Schema.Types.ObjectId, ref: "Post" }, }, { timestamps: true } ); ``` ### تمرين 4: الإعجابات أضف نظام إعجابات. **الحل:** ```javascript // في Post Schema likes: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], // في Controller exports.toggleLike = async (req, res, next) => { const post = await Post.findById(req.params.id); const userId = req.user._id; if (post.likes.includes(userId)) { post.likes = post.likes.filter((id) => id.toString() !== userId.toString()); } else { post.likes.push(userId); } await post.save(); res.json(new ApiResponse(200, post)); }; ``` ### تمرين 5: البحث أضف بحثاً في المقالات. **الحل:** ```javascript exports.searchPosts = async (req, res, next) => { const { q } = req.query; const posts = await Post.find({ $or: [ { title: { $regex: q, $options: "i" } }, { content: { $regex: q, $options: "i" } }, { tags: { $in: [new RegExp(q, "i")] } }, ], }).populate("author", "name email"); res.json(new ApiResponse(200, posts)); }; ``` ## نشر المشروع ### 1. على Railway ```bash # 1. أنشئ حساباً على railway.app # 2. اربط GitHub # 3. اختر المشروع # 4. أضف متغيرات البيئة (MONGODB_URI, JWT_SECRET, ...) ``` ### 2. على Render ```bash # 1. أنشئ حساباً على render.com # 2. New → Web Service # 3. اربط GitHub # 4. أضف Environment Variables ``` ### 3. على Vercel (لـ Serverless) ```javascript // api/index.js const app = require("../src/app"); module.exports = app; ``` ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>إعداد المشروع</td> <td>⬜</td> </tr> <tr> <td>إنشاء Models (User, Post)</td> <td>⬜</td> </tr> <tr> <td>Auth (Register/Login)</td> <td>⬜</td> </tr> <tr> <td>JWT Middleware</td> <td>⬜</td> </tr> <tr> <td>CRUD للمقالات</td> <td>⬜</td> </tr> <tr> <td>رفع الصور</td> <td>⬜</td> </tr> <tr> <td>معالجة الأخطاء</td> <td>⬜</td> </tr> <tr> <td>اختبار API</td> <td>⬜</td> </tr> <tr> <td>نشر المشروع</td> <td>⬜</td> </tr> </tbody> </table> ## 🎉 مبروك! أكملت مسار Node.js! لقد وصلت إلى نهاية المسار. أنت الآن تعرف: - ✅ **أساسيات Node.js:** Modules, fs, npm. - ✅ **HTTP Server:** بناء من الصفر. - ✅ **Express.js:** إطار العمل. - ✅ **Routing & Middleware.** - ✅ **REST API.** - ✅ **MongoDB & Mongoose.** - ✅ **JWT & Authentication.** - ✅ **مشروع متكامل.** ## ماذا بعد Node.js؟ الآن أنت جاهز لـ: 1. **Next.js** — إطار React الكامل. 2. **WebSockets** — الاتصال الفوري. 3. **GraphQL** — بديل REST. 4. **Docker** — الحاويات. 5. **Microservices** — الخدمات المصغرة. 6. **بناء معرض أعمالك.** ## الخلاصة في هذا المشروع، بنيت: - ✅ **API متكامل** مع مصادقة JWT. - ✅ **إدارة المستخدمين** والمقالات. - ✅ **حماية المسارات** والصلاحيات. - ✅ **رفع الملفات.** - ✅ **معالجة الأخطاء** الاحترافية. - ✅ **مشروع جاهز للنشر.** **هذا المشروع هو أساس كل تطبيقات Backend الاحترافية.** احتفظ بالكود، وطور فيه بنفسك!