تصميم 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.jsconst 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`));اختبر فهمك