ما الذي سنبنيه؟
API لإدارة قائمة مقالات (CRUD كامل):
GET /api/articles— جلب كل المقالاتGET /api/articles/:id— جلب مقال محدّدPOST /api/articles— إضافة مقالPUT /api/articles/:id— تحديث مقالDELETE /api/articles/:id— حذف مقال
الإعداد
mkdir api-tutorial
cd api-tutorial
npm init -y
npm install express zod
npm install -D nodemon @types/node typescriptالهيكل المقترح
api-tutorial/
├── src/
│ ├── server.js # نقطة البدء
│ ├── routes/
│ │ └── articles.js # راوتات المقالات
│ ├── middleware/
│ │ └── error.js # معالجة الأخطاء
│ └── data/
│ └── store.js # التخزين (ذاكرة)
└── package.jsonserver.js
import express from "express";
import articlesRouter from "./routes/articles.js";
import { errorHandler } from "./middleware/error.js";
const app = express();
// Middleware عام
app.use(express.json());
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Routes
app.use("/api/articles", articlesRouter);
// معالجة 404
app.use((req, res) => {
res.status(404).json({ error: "المسار غير موجود" });
});
// معالج أخطاء عام — يجب أن يكون الأخير
app.use(errorHandler);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`السيرفر شغّال: http://localhost:${PORT}`);
});data/store.js — تخزين مؤقّت في الذاكرة
let articles = [
{ id: 1, title: "أول مقال", body: "محتوى..." },
{ id: 2, title: "ثاني مقال", body: "محتوى..." },
];
let nextId = 3;
export const store = {
all: () => articles,
find: (id) => articles.find((a) => a.id === Number(id)),
create: (data) => {
const article = { id: nextId++, ...data };
articles.push(article);
return article;
},
update: (id, data) => {
const idx = articles.findIndex((a) => a.id === Number(id));
if (idx === -1) return null;
articles[idx] = { ...articles[idx], ...data };
return articles[idx];
},
delete: (id) => {
const idx = articles.findIndex((a) => a.id === Number(id));
if (idx === -1) return false;
articles.splice(idx, 1);
return true;
},
};routes/articles.js
import { Router } from "express";
import { z } from "zod";
import { store } from "../data/store.js";
const router = Router();
// ===== Validation Schemas =====
const ArticleSchema = z.object({
title: z.string().min(3).max(200),
body: z.string().min(10),
});
// ===== GET /api/articles — كل المقالات =====
router.get("/", (req, res) => {
res.json({
data: store.all(),
count: store.all().length,
});
});
// ===== GET /api/articles/:id — مقال واحد =====
router.get("/:id", (req, res) => {
const article = store.find(req.params.id);
if (!article) {
return res.status(404).json({ error: "غير موجود" });
}
res.json({ data: article });
});
// ===== POST /api/articles — إنشاء =====
router.post("/", (req, res, next) => {
try {
const validated = ArticleSchema.parse(req.body);
const created = store.create(validated);
res.status(201).json({ data: created });
} catch (err) {
next(err);
}
});
// ===== PUT /api/articles/:id — تحديث =====
router.put("/:id", (req, res, next) => {
try {
const validated = ArticleSchema.partial().parse(req.body);
const updated = store.update(req.params.id, validated);
if (!updated) return res.status(404).json({ error: "غير موجود" });
res.json({ data: updated });
} catch (err) {
next(err);
}
});
// ===== DELETE /api/articles/:id =====
router.delete("/:id", (req, res) => {
const ok = store.delete(req.params.id);
if (!ok) return res.status(404).json({ error: "غير موجود" });
res.status(204).end();
});
export default router;middleware/error.js
import { ZodError } from "zod";
export function errorHandler(err, req, res, next) {
console.error(err);
// خطأ تحقّق من البيانات
if (err instanceof ZodError) {
return res.status(400).json({
error: "بيانات غير صالحة",
details: err.issues,
});
}
// أي خطأ آخر
res.status(500).json({
error: "خطأ داخلي في الخادم",
});
}تشغيل وتجربة
# أضف script في package.json
# "dev": "nodemon src/server.js"
npm run devاختبار بـ cURL
# الكل
curl http://localhost:5000/api/articles
# إضافة
curl -X POST http://localhost:5000/api/articles \
-H "Content-Type: application/json" \
-d '{"title":"مقال جديد","body":"محتوى طويل بما يكفي"}'
# تحديث
curl -X PUT http://localhost:5000/api/articles/1 \
-H "Content-Type: application/json" \
-d '{"title":"عنوان محدّث"}'
# حذف
curl -X DELETE http://localhost:5000/api/articles/1أفضل الممارسات
1. الـ HTTP status codes
| الكود | الحالة | |------|--------| | 200 | نجح | | 201 | تمّ الإنشاء | | 204 | نجح بلا محتوى (حذف) | | 400 | طلب سيّء | | 401 | غير مصرّح | | 403 | ممنوع | | 404 | غير موجود | | 500 | خطأ خادم |
2. تنسيق ثابت للرد
// نجح
{ "data": {...}, "meta": { "page": 1 } }
// فشل
{ "error": "رسالة", "details": [...] }3. Pagination للنتائج الكثيرة
router.get("/", (req, res) => {
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const all = store.all();
const start = (page - 1) * limit;
res.json({
data: all.slice(start, start + limit),
meta: {
page,
limit,
total: all.length,
pages: Math.ceil(all.length / limit),
},
});
});4. Rate limiting
npm install express-rate-limitimport rateLimit from "express-rate-limit";
app.use("/api/", rateLimit({
windowMs: 60 * 1000, // دقيقة
max: 100, // 100 طلب/دقيقة
}));ربط PostgreSQL (الخطوة التالية)
استبدل store.js بـ Prisma أو pg:
import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
export const store = {
all: async () => (await pool.query("SELECT * FROM articles")).rows,
find: async (id) => (await pool.query("SELECT * FROM articles WHERE id=$1", [id])).rows[0],
// ...
};الأسئلة الشائعة
REST أم GraphQL؟
- REST: بسيط، معروف، مثالي للـ CRUD
- GraphQL: العميل يطلب ما يحتاج بالضبط، مفيد لواجهات معقّدة
ابدأ بـ REST دائماً. انتقل لـ GraphQL حين تواجه مشاكل over-fetching فعلية.
هل أستخدم Express أم Fastify؟
Express الأكثر شهرةً ومعرفة. Fastify أسرع (2-3x)، أحدث، لكن الموارد أقل.
Authentication؟
للمبتدئين: JWT مع jsonwebtoken. للمشاريع الحقيقية: استخدم مكتبة مختبرة مثل Auth0 أو Clerk.
مقالات ذات صلة
الفرق بين SSR و SSG و ISR: متى تستخدم كل واحد؟
ثلاث استراتيجيات للعرض في Next.js — أيّها الأنسب لموقعك؟ شرح عملي بأمثلة وحالات استخدام حقيقية.
حل مشكلة CORS في Express: الدليل العملي الكامل
واجهت خطأ CORS في تطبيق Node.js؟ تعلّم ما هو، لماذا يحدث، وكيف تُصلحه بطريقة صحيحة (ليس بفتح كل شيء).
React Hooks: شرح useState و useEffect بأمثلة عملية
دليل عملي لأهم hooks في React: useState لإدارة الحالة و useEffect للتعامل مع الآثار الجانبية. أمثلة كاملة بدون حشو.