Kodax

تصميم Routes — REST APIs

تصميم المسارات (Routes)

الموارد المتداخلة

عندما ينتمي مورد لآخر:

GET  /users/42/posts          منشورات المستخدم 42
POST /users/42/posts          إنشاء منشور له
GET  /posts/7/comments        تعليقات منشور

لا تتعمق أكثر من مستويين؛ /users/42/posts/7/comments/3/likes صعب. إذا كان للمورد معرّف فريد عام، استخدمه مباشرة: GET /comments/3.

سلسلة الاستعلام: للفلترة لا للهوية

/products?category=books&minPrice=10     فلترة
/products?sort=-price,name               ترتيب (- تنازلي)
/products?page=2&limit=20                ترقيم
/products?fields=name,price              اختيار حقول

القاعدة: المسار يحدد المورد، والاستعلام يعدّل طريقة عرضه.

الإصدارات

/api/v1/users

عندما تحتاج تغييراً يكسر العملاء الحاليين (حذف حقل، تغيير شكل)، أطلق /api/v2 وأبقِ v1 فترة. التغييرات المتوافقة (إضافة حقل) لا تحتاج إصداراً جديداً.

تنظيم المسارات بـ Router

بدل ملف واحد بمئات المسارات، ملف لكل مورد:

src/
├── app.js
├── routes/
│   ├── users.routes.js
│   └── products.routes.js
└── controllers/
    └── users.controller.js
const express = require("express");

// routes/users.routes.js
const usersRouter = express.Router();
usersRouter.get("/", (req, res) => res.json([]));
usersRouter.get("/:id", (req, res) => res.json({ id: req.params.id }));

// app.js
const app = express();
app.use("/api/v1/users", usersRouter);   // كل مسارات الـ router تبدأ بهذا البادئة

- Routes: أي عنوان يذهب لأي دالة.

- Controllers: منطق معالجة الطلب.

- لاحقاً Services: منطق العمل والوصول للبيانات — فيصبح Controller رقيقاً وقابلاً للاختبار.

ترتيب المسارات مهم

Express يطابق بالترتيب: /users/:id المعرّف قبل /users/me سيلتقط "me" كمعرّف. ضع المسارات الثابتة أولاً.

تمرين

نظّم API الكتب من الدرس السابق في Router منفصل تحت /api/v1/books، وأضف GET /api/v1/authors/:id/books في Router المؤلفين.

const express = require("express");

const books = [
  { id: 1, title: "الأيام", authorId: 1, price: 12, category: "سيرة" },
  { id: 2, title: "مدن الملح", authorId: 2, price: 20, category: "رواية" },
  { id: 3, title: "رجال في الشمس", authorId: 3, price: 9, category: "رواية" },
];

// ===== routes/books.routes.js =====
const booksRouter = express.Router();

booksRouter.get("/", (req, res) => {
  let results = [...books];
  const { category, minPrice, sort } = req.query;

  if (typeof category === "string") results = results.filter((b) => b.category === category);
  if (minPrice !== undefined) {
    const min = Number(minPrice);
    if (Number.isNaN(min)) return res.status(400).json({ error: "minPrice يجب أن يكون رقماً" });
    results = results.filter((b) => b.price >= min);
  }
  if (sort === "price") results.sort((a, b) => a.price - b.price);
  if (sort === "-price") results.sort((a, b) => b.price - a.price);

  res.json(results);
});

booksRouter.get("/:id", (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) return res.status(400).json({ error: "معرّف غير صالح" });
  const book = books.find((b) => b.id === id);
  if (!book) return res.status(404).json({ error: "الكتاب غير موجود" });
  res.json(book);
});

// ===== routes/authors.routes.js =====
const authorsRouter = express.Router();
authorsRouter.get("/:id/books", (req, res) => {
  const id = Number(req.params.id);
  res.json(books.filter((b) => b.authorId === id));
});

// ===== app.js =====
const app = express();
app.use(express.json());
app.use("/api/v1/books", booksRouter);
app.use("/api/v1/authors", authorsRouter);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API على http://localhost:${PORT}/api/v1/books`));
اختبر فهمك