Node.js📅 2026-11-10⏱ 13 دقائق قراءة🟢 مقال 10 من 10

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

مشروع عملي شامل لبناء API متكامل بـ Node.js و Express و MongoDB — مع مصادقة JWT، رفع الملفات، والنشر

🎉 مبروك! وصلت إلى المقال الأخير في مسار 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: إعداد المشروع

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

المكتبات:

المكتبةالوظيفة
expressإطار العمل
mongooseMongoDB
bcryptjsتشفير كلمات المرور
jsonwebtokenJWT
dotenvمتغيرات البيئة
corsالسماح بالطلبات
multerرفع الملفات
helmetحماية الرؤوس
morganتسجيل الطلبات
express-rate-limitحد الطلبات

الخطوة 2: .env

PORT=3000
NODE_ENV=development
MONGODB_URI=mongodb+srv://user:[email protected]/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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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: تشغيل المشروع

npm run dev

اختبار API

1. التسجيل

curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"أحمد","email":"[email protected]","password":"123456"}'

النتيجة:

{
  "success": true,
  "data": {
    "user": { "id": "...", "name": "أحمد", "email": "[email protected]" },
    "token": "eyJhbGciOiJIUzI1NiIs..."
  },
  "message": "تم التسجيل بنجاح"
}

2. تسجيل الدخول

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"123456"}'

3. إنشاء مقال (مع التوكن)

curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"title":"مقالي الأول","content":"هذا محتوى المقال...","published":true}'

4. جلب المقالات

curl http://localhost:3000/api/posts

تمارين إضافية

تمرين 1: التحقق من البريد

أضف خطوة تأكيد البريد الإلكتروني.

الحل:

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: إعادة تعيين كلمة المرور

أضف ميزة "نسيت كلمة المرور".

الحل:

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: التعليقات

أضف نظام تعليقات للمقالات.

الحل:

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: الإعجابات

أضف نظام إعجابات.

الحل:

// في 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: البحث

أضف بحثاً في المقالات.

الحل:

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

# 1. أنشئ حساباً على railway.app
# 2. اربط GitHub
# 3. اختر المشروع
# 4. أضف متغيرات البيئة (MONGODB_URI, JWT_SECRET, ...)

2. على Render

# 1. أنشئ حساباً على render.com
# 2. New → Web Service
# 3. اربط GitHub
# 4. أضف Environment Variables

3. على Vercel (لـ Serverless)

// api/index.js
const app = require("../src/app");
module.exports = app;

قائمة تحقق نهائية

المهمةالحالة
إعداد المشروع⬜
إنشاء Models (User, Post)⬜
Auth (Register/Login)⬜
JWT Middleware⬜
CRUD للمقالات⬜
رفع الصور⬜
معالجة الأخطاء⬜
اختبار API⬜
نشر المشروع⬜

🎉 مبروك! أكملت مسار 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 الاحترافية. احتفظ بالكود، وطور فيه بنفسك!

📚 مقالات ذات صلة

Node.js

MongoDB مع Node.js — دليل شامل 2026

دليل عملي مفصل لاستخدام MongoDB مع Node.js — Mongoose، Schema، CRUD، العلاقات، مع تمارين وحلول

Node.js

بناء REST API في Node.js — دليل شامل 2026

دليل عملي مفصل لبناء REST API احترافي — المبادئ، التنظيم، التحقق، والاختبار، مع تمارين وحلول

Node.js

التوجيه و Middleware في Express.js — دليل شامل 2026

دليل عملي مفصل للتوجيه (Routing) و Middleware في Express.js — Router، تنظيم المسارات، مع تمارين وحلول