بناء REST API كامل بـ Node.js و Express: مشروع عملي
تطوير الويب

بناء REST API كامل بـ Node.js و Express: مشروع عملي

اصنع REST API جاهز للإنتاج بـ Node.js — CRUD كامل، معالجة أخطاء، التحقّق من البيانات، وربط PostgreSQL.

م
مؤسس LahbabiGuide
5 دقائق قراءة
شارك:

ما الذي سنبنيه؟

API لإدارة قائمة مقالات (CRUD كامل):

  • GET /api/articles — جلب كل المقالات
  • GET /api/articles/:id — جلب مقال محدّد
  • POST /api/articles — إضافة مقال
  • PUT /api/articles/:id — تحديث مقال
  • DELETE /api/articles/:id — حذف مقال

الإعداد

bash
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.json
إعلان

server.js

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 — تخزين مؤقّت في الذاكرة

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

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

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: "خطأ داخلي في الخادم",
  });
}

تشغيل وتجربة

bash
# أضف script في package.json
# "dev": "nodemon src/server.js"

npm run dev

اختبار بـ cURL

bash
# الكل
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. تنسيق ثابت للرد

json
// نجح
{ "data": {...}, "meta": { "page": 1 } }

// فشل
{ "error": "رسالة", "details": [...] }

3. Pagination للنتائج الكثيرة

js
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

bash
npm install express-rate-limit
js
import rateLimit from "express-rate-limit";

app.use("/api/", rateLimit({
  windowMs: 60 * 1000,    // دقيقة
  max: 100,               // 100 طلب/دقيقة
}));

ربط PostgreSQL (الخطوة التالية)

استبدل store.js بـ Prisma أو pg:

js
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.

شارك:
المزيد من تطوير الويب
اقرأ أيضاً

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