طراحی API مدیریت کارها؛ از قرارداد تا مدل داده
در این درس ۶ بخش
اکنون یک مسئلهٔ واقعی اما محدود انتخاب میکنیم: فهرست کارهای یک کاربر فرضی. کاربر باید بتواند کاری با عنوان فارسی بسازد، کارها را ببیند و انجامشدن یک کار را ثبت کند. پروژه عمداً ورود کاربر، پایگاه داده و رابط گرافیکی ندارد تا مسیر درخواست تا تغییر داده روشن بماند. همهٔ دادهها در حافظهاند و پس از توقف سرور از بین میروند. برای تمرین از دادهٔ ساختگی استفاده کنید؛ این برنامه محل نگهداری کارهای ضروری یا اطلاعات شخصی نیست.
پوشهای تازه با نام node-task-lab بسازید و در VS Code با Open Folder باز کنید. فایلهای این پنج درس را در همان پوشه و کنار هم نگه دارید. نام فایلها مهم است؛ app.mjs با app.mjs.txt متفاوت است. ابتدا package.json و model.mjs را از همین درس بسازید؛ درس بعد app.mjs و server.mjs را اضافه میکند و درس آزمون، app.test.mjs را میسازد. بنابراین دستور کامل check تا ساختهشدن فایل آزمون هنوز قابل اجرا نیست. برای شروع فقط مدل را بررسی میکنیم.
پیش از نوشتن مسیر، پاسخ مورد انتظار را تعیین کنید
قرارداد ما چنین است: GET /health سلامت فرایند را با ۲۰۰ و ok:true اعلام میکند؛ این نشانهٔ سلامت یک پایگاه داده نیست، چون پایگاه دادهای نداریم. GET /tasks فهرست را در شیء tasks میدهد. POST /tasks فقط شیئی با فیلد title میپذیرد و با وضعیت ۲۰۱ و شیء task پاسخ میدهد. GET /tasks/:id همان کار را میخواند و PATCH /tasks/:id فقط مقدار done را تغییر میدهد. شناسه را سرور تولید میکند؛ مشتری حق تعیین id یا نقش admin را ندارد.
عنوان باید متن باشد، فاصلهٔ ابتدا و انتهایش حذف شود و طول نتیجه بین دو تا صد واحد UTF-16 رشتهٔ جاوااسکریپت باشد؛ این واحد با تعداد حروف قابل مشاهده همیشه برابر نیست. مقدار done فقط boolean واقعی است، پس رشتهٔ "true" پذیرفته نمیشود. فیلد اضافه رد میشود تا اشتباه تایپی بیصدا ناپدید نشود. در این نسخه حذف و جستوجو نداریم؛ نوشتن query string فهرست را فیلتر نمیکند. سقف هزار کار نیز تصمیم آموزشی برای محدودکردن رشد حافظه است.
فایل package.json؛ اجرای روشن و بدون نصب بسته
{
"name": "darsavand-node-task-lab",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": { "node": ">=24" },
"scripts": {
"start": "node server.mjs",
"start:env": "node --env-file=.env server.mjs",
"check": "node --check app.mjs && node --check model.mjs && node --check server.mjs && node --check app.test.mjs",
"test": "node --test app.test.mjs"
}
}
هیچ dependencies یا devDependencies نداریم، بنابراین برای اجرای نسخهٔ همراه نیازی به npm install و دانلود بستهٔ شخص ثالث نیست. private:true از انتشار تصادفی این بسته با npm publish جلوگیری میکند و engines حداقل نسخهٔ مورد انتظار را مستند میکند؛ این فیلد جای بررسی node --version را نمیگیرد. پسوند mjs نوع ماژول را روشن نگه میدارد. دستورهای داخل scripts به برنامهٔ npm سپرده میشوند؛ کاربر ویندوز در PowerShell میتواند npm.cmd را صریح اجرا کند.
فایل model.mjs؛ تنها محل تغییر فهرست
import { randomUUID } from 'node:crypto';
export class AppError extends Error {
constructor(status, code) { super(code); this.status = status; }
}
function exactObject(value, key) {
if (!value || typeof value !== 'object' || Array.isArray(value) ||
Object.keys(value).length !== 1 || !Object.hasOwn(value, key)) {
throw new AppError(422, 'INVALID_FIELDS');
}
}
export function makeStore() {
const tasks = new Map();
return {
list() { return [...tasks.values()].map(task => ({ ...task })); },
get(id) {
const task = tasks.get(id);
if (!task) throw new AppError(404, 'TASK_NOT_FOUND');
return { ...task };
},
create(input) {
exactObject(input, 'title');
if (typeof input.title !== 'string') throw new AppError(422, 'INVALID_TITLE');
const title = input.title.trim();
if (title.length < 2 || title.length > 100) throw new AppError(422, 'INVALID_TITLE');
if (tasks.size >= 1000) throw new AppError(409, 'TASK_LIMIT');
const task = { id: randomUUID(), title, done: false };
tasks.set(task.id, task);
return { ...task };
},
update(id, input) {
exactObject(input, 'done');
if (typeof input.done !== 'boolean') throw new AppError(422, 'INVALID_DONE');
const current = this.get(id);
const task = { ...current, done: input.done };
tasks.set(id, task);
return { ...task };
}
};
}
makeStore هر بار Map تازهای میسازد. پس دو سرور آزمایشی دادهٔ مشترک ندارند. create پس از بررسی داده، شناسهٔ UUID تولید میکند و done را false میگذارد؛ UUID جای احراز هویت نیست و دانستن شناسه مجوز تغییر یک کار واقعی محسوب نمیشود. روش get و خروجی list کپی سادهٔ ردیف برمیگردانند تا تغییر شیء دریافتی، حافظهٔ داخلی را بیخبر عوض نکند. چون ردیف فعلی فقط مقدارهای ساده دارد، همین کپی سطحی برای این قرارداد کافی است.
AppError بین خطای مورد انتظار محصول و خطای پیشبینینشده فرق میگذارد. برای نمونه، عنوان خالی خطای ۴۲۲ دارد و کار ناموجود ۴۰۴ است؛ درس بعد این اطلاعات را به پاسخ HTTP تبدیل میکند. مدل از req و res چیزی نمیداند و به همین دلیل بدون شبکه نیز آزمودنی است. در update مقدار ورودی بررسی میشود و سپس ردیف خوانده و جایگزین میشود؛ ردشدن ورودی باید پیش از تغییر داده رخ دهد. این ترتیب را در بازبینی کد مشخص کنید.
اجرای کوتاه مدل و خروجی قابل بررسی
فعلاً model-demo.mjs را با کد زیر کنار مدل ذخیره کنید و در PowerShell دستور node model-demo.mjs را بزنید. این فایل فقط برای فهم مدل است و جزو چهار فایل اصلی سرور نیست. شناسه هر اجرا عوض میشود، پس مقدار دقیق آن را حفظ نکنید.
import { makeStore } from './model.mjs';
const store = makeStore();
const task = store.create({ title: ' مرور درس ' });
console.log(task.title, task.done); // مرور درس false
store.update(task.id, { done: true });
console.log(store.get(task.id).done); // true
try { store.create({ title: ' ' }); }
catch (error) { console.log(error.status, error.message); }
// 422 INVALID_TITLEتمرین و پاسخ راهنما
یک فیلد اضافی مانند priority را به create بدهید و انتظار را پیش از اجرا بنویسید. پاسخ درست INVALID_FIELDS با وضعیت ۴۲۲ است و تعداد کارها نباید زیاد شود. سپس task.title را در خروجی create عوض کنید و دوباره store.get را بخوانید؛ عنوان ذخیرهشده باید قبلی بماند. معیار تحویل این درس توضیح همین دو مرز است: کدام تابع داده را قطعی میکند و چرا یک کپی دریافتی مالک حافظهٔ داخلی نیست. افزودن فوری فیلدهای بیشتر پیش از روشنشدن قرارداد لازم نیست.
مرجع رسمی
تولید UUID در Node.js: https://nodejs.org/docs/latest-v24.x/api/crypto.html#cryptorandomuuidoptions
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.