مشروع 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 | إطار العمل |
mongoose | MongoDB |
bcryptjs | تشفير كلمات المرور |
jsonwebtoken | JWT |
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؟
الآن أنت جاهز لـ:
- Next.js — إطار React الكامل.
- WebSockets — الاتصال الفوري.
- GraphQL — بديل REST.
- Docker — الحاويات.
- Microservices — الخدمات المصغرة.
- بناء معرض أعمالك.
الخلاصة
في هذا المشروع، بنيت:
- ✅ API متكامل مع مصادقة JWT.
- ✅ إدارة المستخدمين والمقالات.
- ✅ حماية المسارات والصلاحيات.
- ✅ رفع الملفات.
- ✅ معالجة الأخطاء الاحترافية.
- ✅ مشروع جاهز للنشر.
هذا المشروع هو أساس كل تطبيقات Backend الاحترافية. احتفظ بالكود، وطور فيه بنفسك!
📚 مقالات ذات صلة
MongoDB مع Node.js — دليل شامل 2026
دليل عملي مفصل لاستخدام MongoDB مع Node.js — Mongoose، Schema، CRUD، العلاقات، مع تمارين وحلول
بناء REST API في Node.js — دليل شامل 2026
دليل عملي مفصل لبناء REST API احترافي — المبادئ، التنظيم، التحقق، والاختبار، مع تمارين وحلول
التوجيه و Middleware في Express.js — دليل شامل 2026
دليل عملي مفصل للتوجيه (Routing) و Middleware في Express.js — Router، تنظيم المسارات، مع تمارين وحلول