توسعه فرانت‌اند

Node.js و Express: ساخت REST API حرفه‌ای

سرور محلی Express 5 با GET و POST، اعتبارسنجی، JSON errors و حد بدنه؛ نصب و اجرا، مرز ذخیره‌سازی و مسئولیت امنیت تولید.

1404/11/03 3 دقیقه مطالعه 2,077 بازدید

یک API فهرست کارها را برمی‌گرداند و امکان ساخت مورد تازه دارد. مسیرها اجرا می‌شوند، اما ورودی خالی پذیرفته می‌شود، خطای parser به شکل HTML برمی‌گردد و داده پس از راه‌اندازی دوباره از بین می‌رود. ساخت API قابل نگهداری از قرارداد درخواست و نتیجه شروع می‌شود. Express ابزار مسیر و middleware را فراهم می‌کند؛ تصمیم درباره داده، مجوز و رفتار شکست همچنان به برنامه مربوط است.

عنوان «حرفه‌ای» نباید باعث شود یک نمونه کوتاه به‌عنوان سرویس آماده تولید معرفی شود. در این راهنما، نمونه محلی و کوچک است تا ترتیب parser، اعتبارسنجی، مسیر و خطا روشن باشد. داده در حافظه نگهداری می‌شود و احراز هویت ندارد. درباره پایگاه داده، امنیت و عملیات تولید نیز مرزهای لازم توضیح داده می‌شود تا خواننده بداند کدام بخش‌ها باید پیش از عرضه واقعی طراحی شوند.

پیش‌نیاز و نسخه را مشخص کنید

راهنمای نصب Express برای نسخه ۵ حداقل Node.js 18 را بیان می‌کند. حداقل فنی را با انتخاب نسخه تحت پشتیبانی یکی ندانید؛ برای پروژه واقعی، وضعیت runtime را از صفحه انتشارهای Node.js بخوانید. برای نمونه می‌توانید از یک نسخه پشتیبانی‌شده جدید مانند Node.js 24 استفاده کنید. نسخه Node و وابستگی در قرارداد پروژه ثبت شوند.

در پوشه تازه، فرمان npm init -y را اجرا کنید و سپس با npm install express@5 وابستگی را نصب کنید. فایل package-lock.json تولیدشده باید همراه پروژه نگهداری شود تا نسخه دقیق وابستگی‌ها مشخص بماند. کد زیر را به نام app.cjs در همان پوشه ذخیره کنید؛ پسوند cjs قرارداد CommonJS را روشن می‌کند و نیاز به تغییر type در package.json ندارد. این مثال وابستگی دیگری وارد نمی‌کند.

قرارداد دو مسیر ساده

GET روی /items یک آرایه از موردها برمی‌گرداند. POST روی همان مسیر فقط یک object با فیلد title می‌پذیرد و پس از اعتبارسنجی، مورد را می‌سازد. عنوان از ابتدا و انتها trim می‌شود و باید بین یک تا صد کاراکتر داشته باشد. فیلد ناشناخته پذیرفته نمی‌شود. این محدودیت برای مثال آموزشی انتخاب شده و قاعده عمومی همه مدل‌های داده نیست.

ساخت موفق پاسخ 201 و داده مورد تازه دارد. JSON نامعتبر پاسخ 400 می‌گیرد، بدنه بیش از حد پاسخ 413 و نوع محتوا نامناسب پاسخ 415. نقض قرارداد عنوان با 422 بیان می‌شود. وضعیت‌ها باید در مستندات API تعریف شوند تا مصرف‌کننده بتواند میان اصلاح ورودی و تلاش دوباره تفاوت بگذارد. نوشتن پیام یکسان برای همه شکست‌ها، تصمیم سمت client را دشوار می‌کند.

نمونه کامل سرور محلی

با فرمان node app.cjs برنامه را اجرا کنید. سرور فقط روی 127.0.0.1 و پورت 3000 گوش می‌دهد. GET از http://127.0.0.1:3000/items فهرست را نشان می‌دهد. برای POST، یک client HTTP با Content-Type برابر application/json و بدنه object دارای title لازم است؛ برای نمونه title می‌تواند «مرور متن» باشد. این راهنما برای نمایش قرارداد است و فایل برنامه خودش درخواست آزمایشی اجرا نمی‌کند.

const express = require('express');
const { randomUUID } = require('node:crypto');
const app = express();
const items = new Map();
app.disable('x-powered-by');
app.use(express.json({ limit: '16kb' }));
app.get('/items', (req, res) => {
  res.json({ data: [...items.values()] });
});
app.post('/items', (req, res) => {
  if (!req.is('application/json')) {
    return res.status(415).json({ error: { code: 'UNSUPPORTED_MEDIA_TYPE', message: 'Use application/json.' } });
  }
  const body = req.body;
  const validShape = body !== null && typeof body === 'object' && !Array.isArray(body)
    && Object.keys(body).length === 1 && Object.hasOwn(body, 'title');
  const title = validShape && typeof body.title === 'string' ? body.title.trim() : '';
  if (!validShape || title.length < 1 || title.length > 100) {
    return res.status(422).json({ error: { code: 'INVALID_TITLE', message: 'Provide only title, with 1 to 100 characters.' } });
  }
  if (items.size >= 100) {
    return res.status(409).json({ error: { code: 'DEMO_CAPACITY', message: 'The local demo allows 100 items.' } });
  }
  const item = { id: randomUUID(), title };
  items.set(item.id, item);
  return res.status(201).json({ data: item });
});
app.use((req, res) => {
  res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Route not found.' } });
});
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: { code: 'INVALID_JSON', message: 'The request body is not valid JSON.' } });
  }
  if (err.type === 'entity.too.large') {
    return res.status(413).json({ error: { code: 'BODY_TOO_LARGE', message: 'The request body exceeds 16kb.' } });
  }
  console.error('Unhandled request error:', err.name);
  return res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'The request could not be completed.' } });
});
app.listen(3000, '127.0.0.1', () => {
  console.log('Local demo: http://127.0.0.1:3000/items');
});

ظرف items یک Map محلی است. با خاموش کردن فرایند، داده از بین می‌رود؛ اجرای چند نسخه برنامه هم چند حافظه مستقل ایجاد می‌کند. سقف صد مورد فقط از رشد نامحدود این نمونه جلوگیری می‌کند و جای سیاست ظرفیت سرویس نیست. این موارد را با ذخیره‌سازی پایدار اشتباه نگیرید. نمونه عمداً endpointهای ویرایش و حذف را اضافه نکرده تا رفتار دو مسیر معرفی‌شده کامل و قابل دنبال کردن بماند.

ترتیب middleware بر نتیجه اثر دارد

Parser پیش از مسیرهای مصرف‌کننده بدنه قرار می‌گیرد تا JSON قابل بررسی باشد. اگر نوع محتوا JSON نباشد، مسیر ساخت آن را می‌پذیرد؟ در این نمونه پاسخ منفی است و 415 داده می‌شود. بدنه JSON معتبر هم باید از نظر شکل بررسی شود؛ آرایه، null و object با فیلد اضافی به عنوان مدل مورد پذیرفته نمی‌شوند. تشخیص نوع syntax داده با اعتبارسنجی مدل دو مرحله متفاوت‌اند.

Middleware مربوط به مسیر ناموجود پس از مسیرهای تعریف‌شده قرار دارد و پاسخ JSON مشخص می‌دهد. handler خطا پس از آن، چهار پارامتر دارد تا Express آن را به عنوان error middleware بشناسد. اگر پاسخ قبلاً آغاز شده، خطا به next منتقل می‌شود. ترتیب تصادفی این بخش‌ها می‌تواند باعث شود مسیرها هرگز اجرا نشوند یا خطاها با قالبی متفاوت از قرارداد API برگردند.

خطای async و مرزهای گرفتن خطا

راهنمای Error Handling در Express 5 توضیح می‌دهد Promise ردشده در handlerهای بازگرداننده Promise به مسیر خطا منتقل می‌شود. این رفتار را به همه callbackهای جدا از جریان handler تعمیم ندهید. در کار با پایگاه داده و شبکه، تابع و قرارداد async باید روشن باشد. خطایی که در یک کار پس‌زمینه رخ می‌دهد ممکن است مسئولیت مدیریت جداگانه داشته باشد.

اطلاعات فنی خطا را برای رسیدگی ثبت کنید، اما stack trace یا جزئیات خصوصی را در پاسخ عمومی ندهید. شناسه درخواست می‌تواند رابطه پاسخ با گزارش را روشن کند. در نمونه، خطای ناشناخته با پیام عمومی 500 برمی‌گردد. در تولید، قالب خطا بهتر است code پایدار و توضیح قابل استفاده داشته باشد. مصرف‌کننده نباید مجبور شود متن طبیعی پیام را برای تشخیص نوع خطا parse کند.

ساختار لایه‌ها از نیاز پروژه می‌آید

Controller، Service و Repository می‌توانند مسئولیت مسیر، منطق کسب‌وکار و دسترسی داده را جدا کنند، اما استفاده از سه فایل برای هر تابع الزام کیفیت نیست. نمونه کوچک می‌تواند ساده بماند. وقتی قواعد مستقل، چند منبع داده یا استفاده مجدد وجود دارد، جداسازی معنا پیدا می‌کند. نام لایه بدون مرز روشن تنها رفت‌وبرگشت میان فایل‌ها را زیاد می‌کند.

Service بهتر است قرارداد مستقل از درخواست HTTP داشته باشد تا منطق آن به req و res وابسته نشود، اگر چنین جداسازی لازم است. Repository هم باید درباره نتیجه ناموجود، خطا و تراکنش قرارداد داشته باشد. در داده پایدار، تغییر چند موجودیت ممکن است تراکنش بخواهد. راهنمای الگوهای JavaScript برای تفکیک مسئولیت‌ها مکمل است، ولی هیچ pattern به‌تنهایی رفتار درست داده را تضمین نمی‌کند.

احراز هویت با مجوز یکی نیست

دانستن هویت درخواست‌کننده به معنی اجازه دسترسی به هر رکورد نیست. برای مسیر دارای شناسه، مالکیت و مجوز عمل را بررسی کنید. نمونه این مقاله چنین سیاستی ندارد و صرف اتصال محلی آن را به API مناسب داده خصوصی تبدیل نمی‌کند. در سرویس واقعی، نشست یا token، زمان اعتبار و ابطال باید طراحی شوند و اطلاعات محرمانه در log یا پیام خطا ثبت نشوند.

CORS سیاست خواندن پاسخ توسط مرورگر است و جای مجوز سمت سرور را نمی‌گیرد. مهاجم غیرمرورگری می‌تواند درخواست ارسال کند. محدودیت نرخ، هدرهای امنیتی و TLS هم هرکدام دامنه جدا دارند. راهنمای امنیت Express این موضوع‌ها را شرح می‌دهد. افزودن Helmet می‌تواند بخشی از هدرها را تنظیم کند، اما وعده امن شدن کامل API با نصب یک بسته دقیق نیست.

ظرفیت و ورودی را محدود کنید

حد اندازه بدنه از دریافت بی‌حد داده جلوگیری می‌کند، ولی محدودیت‌های مدل، تعداد موارد و هزینه عملیات نیز لازم‌اند. queryها باید مقدارهای مجاز و سقف داشته باشند. برای فهرست بزرگ، صفحه‌بندی و قرارداد ترتیب مهم است. پاسخ دادن همه داده در یک درخواست ممکن است هزینه حافظه و شبکه بسازد. محدوده‌ای که برای نمونه مناسب است نباید بی‌دلیل به سیاست تولید تبدیل شود.

تلاش دوباره در درخواست تغییر داده می‌تواند عمل را تکرار کند. برای عملیات حساس، قرارداد idempotency و شناسه مناسب لازم است. قطع اتصال client به معنی لغو قطعی تغییر سمت سرور نیست. در طراحی نتیجه، وضعیت نامعلوم و راه پیگیری را در نظر بگیرید. این مسئله در مسیر پرداخت اهمیت بیشتری دارد، ولی برای هر عملیات دارای اثر خارجی قابل توجه است.

مستندات باید حالت شکست را هم پوشش دهند

برای هر مسیر، method، نوع محتوا، ورودی، خروجی و وضعیت‌های ممکن را بنویسید. مثال موفق بدون مثال ورودی نامعتبر، قرارداد را ناقص می‌گذارد. تفاوت نبودن رکورد و نداشتن دسترسی را متناسب با سیاست اطلاعات محصول تعیین کنید. زمان اعتبار داده، ترتیب فهرست و رفتار صفحه‌بندی نیز باید معلوم باشند. Schema می‌تواند کمک کند، ولی شرح نیاز کسب‌وکار همچنان لازم است.

در تحویل تولید، تنظیم محیط، ذخیره‌سازی، shutdown مناسب، گزارش خطا و پایش باید مسئول مشخص داشته باشند. نمونه محلی پایه‌ای برای فهم جریان درخواست است و این مسئولیت‌ها را اجرا نمی‌کند. API قابل نگهداری از هماهنگی قرارداد، اعتبارسنجی و نتیجه واقعی ساخته می‌شود. Express این مسیر را فراهم می‌کند؛ کیفیت سرویس به تصمیم‌هایی وابسته است که درباره داده، دسترسی و شکست در همان پروژه گرفته می‌شوند.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!