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