ساخت یک API سریع با Fastify.js

Create a Fast API with Fastify.js

ساخت یک API سریع با Fastify.js

این مقاله برای افراد ناآشنا و تازه کار نوشته نشده است. پیش نیاز این مقاله این است که با مفاهیم کلی توسعه وب آشنا باشید. همچنین آشنایی با فریم ورک های back-end جاوا اسکریپتی مانند Express.js کمک بزرگی به درک شما از این مقاله خواهد کرد.

Fastify چیست؟

Fastify یک فریم ورک محبوب برای زبان Node.js و توسعه برنامه های تحت وب است. اگر با زبان Node.js کار کرده باشید حتما با Express.js و Hapi (دو فریم ورک دیگر برای Node.js) آشنا هستید. Fastify به شدت از Express.js و Hapi الهام گرفته است و ساختار های بسیار مشابهی با آن ها دارد. اگر بخواهیم بدون در نظر گرفتن جزئیات، تنها محبوبیت این سه فریم ورک را بررسی کنیم، می توانیم به ستاره های گیت هاب آن ها نگاهی بیندازیم:

  • Express.js در حال حاضر محبوب ترین فریم ورک Node.js با ۵۴ هزار ستاره است.
  • Hapi نیز یک فریم ورک محبوب با ۱۳ هزار ستاره است.
  • Fastify با ۱۸ هزار ستاره از Hapi محبوب تر است اما فاصله زیادی با Express دارد.

یکی از حوزه هایی که تیم Fastify اهمیت زیادی به آن داده اند، سرعت بالای آن است. تیم توسعه Fastify این سرعت بالا را به عنوان یکی از نقاط تبلیغات خود بدل کرده است تا جایی که یک صفحه اختصاصی را به benchmark های مختلف از فریم ورک های مختلف Node.js اختصاص داده است. بر اساس ادعای تیم Fastify اعداد به دست آمده در این تست ها به شکل زیر است:

  • نسخه ۳.۱۰.۱ از Fastify قابلیت پردازش 64683.2 درخواست بر ثانیه را دارد و latency (تاخیر) آن نیز پایین بوده است.
  • نسخه ۲.۱۳.۱ از Koa قابلیت پردازش 41324.2 درخواست بر ثانیه را دارد.
  • نسخه ۲۰.۰.۳ از Hapi توانایی پردازش 30830.8 درخواست بر ثانیه را دارد.
  • نسخه ۴.۱۷.۱ از Express.js توانایی پردازش 11795.6 درخواست بر ثانیه را دارد که در صورت اضافه شدن middleware به آن به 10241.3 درخواست بر ثانیه کاهش پیدا می کند.

تاریخ انجام این تست ها در ۱۶ ژانویه سال ۲۰۲۱ بوده است. البته Fastify قابلیت های دیگری نیز دارد. به طور مثال از تایپ اسکریپت پشتیبانی کرده و دارای hook و plugin و decorator است. همچنین یادگیری آن بسیار آسان و شبیه به Express.js است و به کاربران تازه کار اجازه می دهد برنامه هایی با حجم بسیار بالا بسازند. به همین دلیل من تصمیم گرفتم که یک API ساده را با استفاده از Fastify بسازیم تا با کلیت آن آشنا شویم.

پنج پلاگین مهم برای Fastify

قبل از ادامه بحث بهتر است با ۵ پلاگین کاربردی از Fastify آشنا بشوید. پلاگین هایی که در گیت هاب و فضای وب برای Fastify مشاهده می کنید معمولا دو نوع هستند: community plugins ها که توسط طرفداران این فریم ورک توسعه داده شده اند و core plugins که توسط تیم توسعه Fastify توسعه داده شده اند. گرچه ما در این جلسه می خواهیم یک API بسازیم و از این پلاگین ها استفاده نخواهیم کرد اما آن ها را معرفی می کنم تا اگر خواستید از Fastify استفاده کنید با این پلاگین ها آشنا باشید.

پلاگین Fastify-auth: این پلاگین یک پلاگین core است بنابراین توسط خود تیم fastify ارائه شده است. این پلاگین به شما اجازه می دهد که سریعا منطق احراز هویت را در route های مختلف خود تزریق کنید.

پلاگین fastify-cors: حتما با مفهوم درخواست های Cross-origin آشنا هستید. زمانی که ما در حال توسعه front-end هستیم در اکثر اوقات داده ای را نمایش می دهیم که جای دیگری (روی سرور دیگری) قرار دارد. به همین منظور مرورگر باید ابتدا یک درخواست HTTP را به سرور مقصد ارسال کند تا تمام داده های مورد نیاز ما را دریافت کند. ما در اینجا نام فرضی www.mywebsite.com را برای وب سایت خود در نظر می گیریم. آدرس API این وب سایت را نیز api.website.com در نظر می گیریم. زمانی که درخواستی به این API ارسال می شود، داده های ما به صورت JSON برای کاربر (هر کسی که درخواست را ارسال کرده است) برگردانده خواهند شد. ما در حال حاضر روی سایت www.mywebsite.com هستیم اما اگر این بار درخواست خود را به دامنه دیگری مانند www.anotherdomain.com ارسال کنیم چه می شود؟ با اجرای این درخواست خطایی به شکل Access to fetched has been blocked by CORS policy دریافت می کنیم چرا که مرورگر ها به دلایل امنیتی درخواست های CORS را محدود می کنند. CORS مخفف Cross-origin resource sharing یا به اشتراک گذاری منابع از چند سورس مختلف است. پکیج Fastify-cors به شما اجازه می دهد بدون نیاز به پکیج های جانبی دیگر، درخواست های CORS خود را مدیریت کنید.

پلاگین fastify-jwt: اگر با توسعه وب و مخصوصا توسعه API آشنا هستید، حتما با مفهوم JSON WEB TOKEN یا JWT آشنا هستید. پکیج fastify-jwt به شما اجازه می دهد که از JWT در برنامه خود استفاده نمایید.

پلاگین fastify-nextjs: اگر با فریم ورک React کار کرده باشید احتمالا نام Next را شنیده اید. Next به پروژه React شما اجازه می دهد که محتوای خود را در سمت سرور render کرده و صفحات از قبل render شده را برای کاربران ارسال کند. معمولا کسانی که با فریم ورک های جاوا اسکریپتی مانند Vue یا React کار می کنند از این روش برای ارتقاء سئوی سایت خود استفاده می کنند. پکیج fastify-nextjs به شما اجازه می دهد که این کار را درون Fastify انجام بدهید.

پلاگین fastify-redis: این پلاگین به شما اجازه می دهد که یک اتصال واحد به پایگاه داده Redis خود را در تمام قسمت های برنامه خود به اشتراک بگذارید (استفاده از یک اتصال در سر تا سر برنامه).

نکته عجیب تر اینجاست که این ۵ پلاگین فقط بخش کوچکی از پلاگین های موجود در Fastify هستند. اگر به صفحه اکوسیستم fastify در وب سایت رسمی Fastify مراجعه کنید لیست بلند و بالایی از انواع و اقسام پلاگین های fastify را مشاهده خواهید کرد. در همین صفحه توضیح داده شده است که تعداد core plugin ها (پلاگین هایی که توسط خود تیم fastify ساخته شده اند) ۴۴ عدد است و علاوه بر آن ۱۳۴ پلاگین community (ساخته شده توسط افراد دیگر و طرفداران این فریم ورک) نیز وجود دارد! به طور مثال پلاگین مشهور fastify-csrf صفحات شما را در مقابل حملات CSRF محفوظ می کند.

شاید بپرسید چرا این همه پلاگین وجود دارد؟ همانطور که گفتم سرعت بالا در فریم ورک fastify یکی از نقاط اصلی تبلیغ برای تیم fastify بوده است. ایده تیم توسعه fastify این است که همه چیز یک پلاگین است! مزیت این روش را زمانی درک می کنید که در حال توسعه یک پروژه هستید و متوجه می شوید که به جز موارد مورد نیاز خود هیچ چیز دیگری روی سیستم شما نصب نیست! در بسیاری از فریم ورک های دیگر قابلیت هایی وجود دارد که آنچنان مورد نیاز ما نیست اما سرعت سرور را کاهش می دهند اما چنین مشکلی در fastify وجود ندارد.

آماده سازی محیط پروژه و نصب وابستگی ها

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

  • آشنایی با نصب و راه اندازی Fastify
  • آشنایی با نحوه تعریف route در Fastify
  • آشنایی با نحوه اعتبار سنجی (schema validation) در Fastify
  • آشنایی با نحوه استفاده از پلاگین های Fastify
  • آشنایی با نحوه تعریف Hook ها در Fastify

حالا که پروژه خود را هدف گذاری کرده ایم باید شروع به کار کنیم.

نصب Node.js

در ابتدا به وب سایت Node.js بروید و آخرین نسخه آن را نصب کنید. در صورتی که بنا به دلایلی باید نسخه ای قدیمی از Node.js را روی سیستم خود داشته باشید می توانید از ابزاری به نام NVM یا Node Version Manager استفاده نمایید. این ابزار به شما اجازه می دهد که از نسخه خاصی از Node.js در پروژه های مختلف استفاده کنید. کاربران لینوکس برای نصب NVM می توانند دستور زیر را اجرا کنند:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash

در مرحله بعدی دستور زیر را اجرا کنید:

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

در مرحله بعدی آخرین نسخه از Node را نصب می کنیم:

nvm install node # "node" is an alias for the latest version

شما می توانید به جای node از عدد خاصی استفاده کنید که نسخه خاصی از Node.js را مشخص می کند. با انجام این کار همان نسخه خاص برایتان نصب می شود اما node خالی به معنای نصب آخرین نسخه است. حالا باید مشخص کنید که از کدام نسخه از Node استفاده خواهیم کرد:

nvm use node

باز هم node خالی به معنای آخرین نسخه است اما اگر نسخه خاصی مد نظر شما است باید به جای node همان عدد خاص را بگذارید.

نصب ابزار تست API

برای تست API ای که نوشته ایم باید یک ابزار تست API داشته باشیم چرا که API ها مانند وب سایت های عادی نیستند که با مرورگر تست بشوند، بلکه نرم افزار های خاصی می خواهند که درخواست های ما را به API ارسال کنند. ابزار های بسیار مشهوری در این زمینه وجود دارد اما من دو نرم افزار زیر را پیشنهاد می کنم:

  • Postman: یکی از مشهور ترین ابزار های تست API به حساب می آید و نسخه ای رایگان نیز دارد که برای کار ما مناسب است.
  • Insomnia: از ابزار های پر طرفدار تست API است و نسبت به Postman کمی سریع تر بارگذاری می شود بنابراین برای افرادی که سیستم های ضعیف تری دارند مناسب تر است. این برنامه نیز نسخه ای رایگان دارد.

شما می توانید هر کدام از این دو ابزار را نصب کنید تا آماده کار شویم. این دو ابزار کار ما را بسیار ساده تر می کنند اما اگر به دلیل توانایی نصب آن ها یا نرم افزار های مشابه را ندارید می توانید از cURL نیز استفاده کنید که فقط در ترمینال کار می کند و دانش تخصصی می خواهد.

آغاز یک پروژه npm

حالا که وابستگی های سیستم را نصب کرده ایم باید در ابتدا پروژه خود را با npm مدیریت کنیم. برای این کار در پوشه مورد نظر خودتان ترمینال را باز کرده و دستور زیر را اجرا نمایید:

npm init -y

این دستور یک پروژه npm را برایتان ایجاد می کند. در مرحله بعدی باید fastify را نصب کنیم بنابراین:

npm i fastify --save

حتما save-- را نیز در آن قرار بدهید تا به عنوان یک dependency یا وابستگی اصلی برنامه شناخته شود. اگر از کاربران yarn هستید باید به جای دستور بالا از دستور زیر استفاده کنید:

yarn add fastify

طبیعتا می توانید با ابزار NVM نیز ورژن Node.js خود را تغییر بدهید.

ساخت یک سرور با Fastify

در مرحله بعدی یک فایل ساده به نام index.js را بسازید. البته می توانید از هر نام دیگری مانند server.js نیز استفاده نمایید اما من index.js را انتخاب می کنم. در مرحله اول باید پکیج fastify را در این فایل import کرده و یک سرور ساده بسازیم:

const app = require("fastify")({

  logger: true,

});







app.listen(3000, (err, address) => {

  if (err) {

    app.log.error(err);

    process.exit(1);

  }

  app.log.info(`server listening on ${address}`);

});

همانطور که در کد بالا مشاهده می کنید من ابتدا پکیج fastify را وارد کرده ام و سپس آن را مانند یک تابع با نوشتن پرانتز ها صدا کرده ام. یکی از گزینه هایی که می توانیم به آن پاس بدهیم گزینه logger است. logger وقایع اتفاق افتاده در سرور را برای شما log می کند اما به صورت پیش فرض خاموش است بنابراین حتما باید مقدار true را به آن بدهیم تا فعال شود. چرا؟ به دلیل اینکه ما در حال توسعه برنامه خودمان هستیم و قطعا نیاز داریم که تمام اتفاقات رخ داده در سمت سرور را مشاهده کنیم. از این به بعد متغیر app همان برنامه fastify ما خواهد بود بنابراین app.listen را صدا زده ام تا یک سرور ساده را راه اندازی کنیم. آرگومان اول listen همان پورتی است که برنامه شما باید روی آن اجرا شود. من پورت ۳۰۰۰ را انتخاب کرده ام اما شما می توانید از پورت های دیگر نیز استفاده کنید. آرگومان دوم یک callback است که دو آرگومان err (خطاهای احتمالی) و address (آدرس سرور برنامه شما) را دریافت می کند. من با یک شرط if ساده گفته ام اگر خطایی وجود داشته باشد باید به عنوان خطا log شود (app.log.error) و سپس از پروسه خارج شویم یا به زبان ساده تر سرور بسته شود (process.exit). در غیر این صورت آدرس سرور ما به صورت info (اطلاعات) برایمان log خواهد شد.

بیایید قبل از هر کاری این برنامه را تست کنیم. برای این کار به ترمینال (یا CMD برای کاربران ویندوز) رفته و می گوییم:

node index.js

اگر تمام مراحل را به درستی انجام داده باشید چنین نتیجه ای را در ترمینال خود دریافت خواهید کرد:

{"level":30,"time":1616740824947,"pid":13800,"hostname":"HP","msg":"Server listening at http://127.0.0.1:3000"}

{"level":30,"time":1616740824948,"pid":13800,"hostname":"HP","msg":"server listening on http://127.0.0.1:3000"}

همانطور که می بینید اطلاعاتی کلی مانند hostname (نام هاست) و زمان آن و آدرس سرور برایمان چاپ شده است (http://127.0.0.1:3000) بنابراین همه چیز موفقیت آمیز بوده است. احتمالا برایتان سوال شده است که چرا دو بار اطلاعات سرور log شده است؟ زمانی که logger را روی true می گذارید اطلاعات سرور به صورت پیش فرض log می شوند بنابراین توضیحات اول مربوط به آن قسمت است و توضیحات دوم مربوط به دستور app.log.info خودمان می باشد که برای محکم کاری نوشته ایم.

در نظر داشته باشید که fastify به صورت پیش فرض از async/await پشتیبانی می کند بنابراین اگر دوست ندارید از callback بالا استفاده کنید می توانید کد خود را به شکل زیر بنویسید:

const app = require("fastify")({

  logger: true,

});




const start = async () => {

  try {

    await app.listen(3000);

  } catch (err) {

    app.log.error(err);

    process.exit(1);

  }

};

start();

حتما می دانید که async/await فقط در توابع قابل استفاده هستند بنابراین من یک تابع async تعریف کرده ام که در آن از try & catch استفاده کرده ایم. تنها تفاوت اینجاست که دستور app.listen را await کرده ایم. اگر همه چیز موفق باید دستور listen یک تابع را به start پاس می دهد بنابراین متغیر start حالا یک تابع است و با صدا زدن آن، سرور اجرا می شود.

تعریف یک Route ساده

حالا که می دانیم سرور ما به درستی کار می کند باید یک route یا مسیر را نیز تعریف کنیم:

// Require the framework and instantiate it

const app = require("fastify")({

  logger: true,

});




// Declare a route

app.get("/", function (req, reply) {

  reply.send({ hello: "world" });

});




// Run the server!

app.listen(3000, (err, address) => {

  if (err) {

    app.log.error(err);

    process.exit(1);

  }

  app.log.info(`server listening on ${address}`);

});

همانطور که می بینید ساختار تعریف route در fastify دقیقا مانند express.js است؛ یعنی نام برنامه (متغیر app) را صدا زده و سپس متد درخواست (GET یا POST یا هر متد دیگری) را به عنوان یک تابع صدا می زنید. آرگومان اول همان مسیر شما است و آرگومان دوم تابعی است که req (شیء درخواست) و reply (شیء پاسخ) را دریافت می کند. من reply.send را صدا زده ام که یک پاسخ را به سمت کاربر ارسال می کند. این پاسخ چیست؟ یک شیء ساده که در کد بالا مشخص است. شما می توانستید به جای این شیء یک رشته عادی یا هر پاسخ دیگری ارسال کنید.

حالا اگر سرور شما در حال اجرا است، آن را متوقف کرده (کلید های Ctrl + C در ترمینال) و دوباره آن را با دستور node index.js اجرا کنید. حالا اگر مرورگر خود را باز کرده و به آدرس http://localhost:3000/ بروید، نتیجه زیر را دریافت می کنید:

{

hello: "world"

}

این همان شیء ای است که به عنوان پاسخ ارسال کرده بودیم. حالا اگر به ترمینال خود نگاه کنید یک log از این وقایع را می بینید:

{"level":30,"time":1616744345634,"pid":15139,"hostname":"HP","reqId":"req-1","req":{"method":"GET","url":"/","hostname":"localhost:3000","remoteAddress":"127.0.0.1","remotePort":35530},"msg":"incoming request"}

{"level":30,"time":1616744345684,"pid":15139,"hostname":"HP","reqId":"req-1","res":{"statusCode":200},"responseTime":49.03304699994624,"msg":"request completed"}

{"level":30,"time":1616744345932,"pid":15139,"hostname":"HP","reqId":"req-2","req":{"method":"GET","url":"/favicon.ico","hostname":"localhost:3000","remoteAddress":"127.0.0.1","remotePort":35538},"msg":"incoming request"}

{"level":30,"time":1616744345932,"pid":15139,"hostname":"HP","reqId":"req-2","msg":"Route GET:/favicon.ico not found"}

{"level":30,"time":1616744345933,"pid":15139,"hostname":"HP","reqId":"req-2","res":{"statusCode":404},"responseTime":0.7281730007380247,"msg":"request completed"}


اولین تجربه کار با Insomnia

تست این مسیر ساده با مرورگر کار دشواری نبود چرا که فقط یک پاسخ ساده را داشتیم اما ارسال درخواست های POST و تست API غیرممکن می شود. به همین دلیل است که به Insomnia نیاز داریم. برای تست API خودمان با Insomnia ابتدا برنامه را باز کرده و درخواست های مختلف آن برای عضویت را skip کنید. با ورود به برنامه (در سمت بالا و راست صفحه) گزینه ای به نام Create وجود دارد. روی آن کلیک کرده و از منوی باز شده گزینه Request Collection را انتخاب نمایید. با انجام این یک صفحه برایتان باز می شود و از شما می خواهد که نامی را برای مجموعه درخواست خود انتخاب کنید. شما می توانید هر نامی را انتخاب کنید (من شخصا نام Fastify-Roxo را انتخاب کرده ام).

در مرحله بعدی وارد محیط جدیدی خواهید شد. در این صفحه روی علامت + کلیک کرده و گزینه New Request را انتخاب کنید. با انجام این کار پنجره جدیدی باز می شود که از شما می خواهد نام این درخواست خاص را انتخاب کنید. من نام First GET Test را انتخاب می کنم و از ستون سمت راست گزینه GET را انتخاب می کنم (گرچه باید به صورت پیش فرض روی GET باشد) تا مشخص کنیم که نوع درخواست ما GET است. در نهایت روی Create کلیک می کنیم.

با این کار درخواست جدید ما ساخته می شود. حالا URL درخواست (http://localhost:3000) را در این صفحه اضافه کنید و روی گزینه Send کلیک کنید:

ارسال اولین درخواست در برنامه ی Insomnia به سرور توسعه
ارسال اولین درخواست در برنامه Insomnia به سرور توسعه

زمانی که مانند من آدرس درخواست را اضافه کرده و روی Send کلیک کنید، همان پاسخ قبلی (شیء hello world) را دریافت می کنید. البته زمان ارسال پاسخ نیز برایتان مشخص شده و حجم درخواست به بایت نیز نمایش داده می شود. همچنین اگر به سربرگ header بروید می توانید header های درخواست را نیز مشاهده کنید که برای من بدین شکل هستند:

content-type: application/json; charset=utf-8

content-length: 17

Date: Fri, 26 Mar 2021 08:12:27 GMT

Connection: keep-alive

Keep-Alive: timeout=5

همچنین تمام وقایع مانند قبل در ترمینال Log شده است. ما از این به بعد از Insomnia استفاده کرده و از همین روش جلو می رویم چرا که تست API با مرورگر (به جز موارد ساده مانند درخواست های GET) غیر ممکن است.

آشنایی با route های برنامه

ممکن نیست هیچ API ای تنها دارای مسیر های GET باشد چرا که در این حالت مسیر ارتباط یک طرفه خواهد بود. من مسیر های زیر را برای API خودمان در نظر گرفته ام:

  • یک مسیر GET برای دریافت تمام پست ها به مسیر /api/blogs
  • یک مسیر GET برای دریافت یک پست خاص به مسیر /api/blogs/:id
  • یک مسیر POST برای ثبت کردن پست جدید به مسیر/api/blogs
  • یک مسیر PUT برای ویرایش کردن پستی خاص به مسیر /api/blogs/:id
  • یک مسیر DELETE برای حذف کردن پستی خاص به مسیر /api/blogs/:id

دقت کنید که قرار دادن علامت دو نقطه در کنار مسیر مانند id: به معنی این است که id: یک URL Parameter است. یعنی چه؟ یعنی id واقعا id نیست و نمی توانیم به آدرسی مثل api/blogs/id برویم بلکه id مانند یک متغیر است که باید مقدار واقعی اش را به آن بدهید. مثلا api/blogs/2 یعنی id=2 را می خواهیم!

تعریف تمام route handler های برنامه

ما می خواهیم کد هایمان را تمیز و دسته بندی شده نگه داریم بنابراین در پوشه اصلی پروژه خود (در کنار فایل index.js) پوشه جدیدی به نام controller بسازید و درون آن فایلی به نام blogs.js ایجاد کنید. از آنجایی که اتصال پایگاه داده به Fastify بحث جداگانه ای است من نمی خواهم آن را وارد این مقاله کنم بنابراین داده های ساده ای را آماده کرده ایم که شبیه ساز پایگاه داده باشد و آن ها را درون این فایل (blogs.js) قرار می دهیم:

// Demo data

let blogs = [

  {

    id: 1,

    title: "This is an experiment from fastify-roxo project",

  },

  {

    id: 2,

    title: "Fastify is pretty cool",

  },

  {

    id: 3,

    title: "Just another blog, yea!",

  },

];

این مقاله مخصوص یادگیری Fastify است و اتصال پایگاه داده هایی مانند MySQL یا MongoDB ما را از هدف اصلی دور می کند بنابراین به سراغ آن ها نمی روم اما باید بدانید که انجام این کار ساده است و نیازی به دانش خاصی ندارد.

در مرحله بعدی باید handler های این مسیر ها را تعریف کنیم. handler به توابعی گفته می شود که مسئول مدیریت درخواست های وارد شده به یک مسیر خاص هستند. مهم نیست ابتدا route ها (مسیر هایتان) را تعریف کنید یا ابتدا handler ها را تعریف کنید. از آنجایی که من عادت به تعریف handler ها قبل از route ها دارم ابتدا در همین فایل blogs.js شروع به تعریف آن ها می کنم:

// Demo data

let blogs = [

  {

    id: 1,

    title: "This is an experiment from fastify-roxo project",

  },

  {

    id: 2,

    title: "Fastify is pretty cool",

  },

  {

    id: 3,

    title: "Just another blog, yea!",

  },

];




// Handlers

const getAllBlogs = async (req, reply) => {

    return blogs

}

در اینجا متدی به نام getAllBlogs را تعریف کرده ام که تابعی async است و نهایتا آرایه blogs را برمی گرداند. handler بعدی ما بدین شکل است:

const getBlog = async (req, reply) => {

    const id = Number(req.params.id) // blog ID

    const blog = blogs.find(blog => blog.id === id)

    return blog

}

همانطور که قبلا هم توضیح دادم هر route handler (توابعی که درخواست های یک route را مدیریت می کنند - همین توابعی که در حال نوشتن آن ها هستیم) یک تابع است که به صورت پیش فرض دو آرگومان req (شیء درخواست) و reply (شیء پاسخ) را دریافت می کند. روی شیء درخواست خصوصیتی به نام params وجود دارد که به شما اجازه می دهد به URL Parameter ها (مانند id: که قبلا توضیح دادم) دسترسی داشته باشید و بتوانید آن را استخراج کنید. ما با تابع جاوا اسکریپتی find سعی در پیدا کردن آن id خاص کرده و نتیجه را به کاربر برمی گردانیم. البته شما در پروژه های واقعی خود بهتر است نتیجه تابع find را بررسی کنید و در صورت خالی بودن متغیر blog پاسخ متناسبی را به کاربر برگردانید.

route handler بعدی ما مربوط به اضافه کردن یک پست جدید است:

const addBlog = async (req, reply) => {

    const id = blogs.length + 1 // generate new ID

    const newBlog = {

        id,

        title: req.body.title

    }




    blogs.push(newBlog)

    return newBlog

}

برای ساخت یک پست جدید ابتدا باید یک id جدید داشته باشیم. برای تولید id جدید می توانیم طول این آرایه را با یک جمع کنیم و آن را به عنوان آیدی جدید در نظر بگیریم. در مرحله بعدی شیء ای می سازیم که علاوه بر id یک title داشته باشد. محتویات title از req.body (بدنه درخواست از شیء درخواست) استخراج می شود. طبیعتا در یک برنامه واقعی باید این داده ها را اعتبارسنجی می کردیم اما در این برنامه تستی نیاز به انجام این کار نیست. در ضمن شاید بپرسید خصوصیت title از کجا روی req.body به وجود آمده است؟ این خصوصیت توسط ما پاس داده خواهد شد، یعنی بدنه درخواست ارسال ما باید این خصوصیت را داشته باشد. در نهایت این آیتم جدید را به آرایه blogs اضافه کرده (متد push) و آرایه جدید را برمی گردانیم.

route handler بعدی مربوط به ویرایش یک پست است:

const updateBlog = async (req, reply) => {

    const id = Number(req.params.id)

    blogs = blogs.map(blog => {

        if (blog.id === id) {

            return {

                id,

                title: req.body.title

            }

        }

    })




    return {

        id,

        title: req.body.title

    }

}

مقادیری که از سمت req.params دریافت می شوند به صورت رشته هستند بنابراین من id دریافت شده را به تابع number پاس داده ام تا تبدیل به عدد شود. در مرحله بعدی از متد map استفاده کرده ایم تا روی تمام پست ها گردش کنیم. در هر گردش به دنبال id خاصی می گردیم که از سمت کاربر دریافت شده است. هر پستی که دارای این آیدی بود، پست مورد نظر ما برای ویرایش است بنابراین آن را با مقادیر جدید ویرایش کرده ایم و نهایتا این پست جدید را return کرده ایم تا کاربر نتیجه تغییرات را ببیند.

در نهایت route handler ای را برای حذف پست ها می خواهیم:

const deleteBlog = async (req, reply) => {

    const id = Number(req.params.id)




    blogs = blogs.filter(blog => blog.id !== id)

    return { msg: `Blog with ID ${id} is deleted` }

}

مثل همیشه ابتدا id را دریافت کرده و سپس با استفاده از تابع جاوا اسکریپتی filter به دنبال آن آیدی خاص در بین پست ها می گردیم. هر پستی که موافق با شرط ما باشد (blog.id !== id) از بین پست ها حذف خواهد شد و آرایه جدید ما در متغیر blogs قرار می گیرد. توجه کنید که blogs همان آرایه صلی بود که در ابتدای کار تعریف کردیم. حالا با یک پیام ساده به کاربر می گوییم که پستی با فلان آیدی حذف شده است.

با انجام این کار فقط یک سری تابع عادی تعریف کرده ایم و هنوز تبدیل به route handler نشده اند! برای اینکه این توابع به route handler تبدیل شوند باید هر تابع را به route (مسیر) مناسب خودش متصل کنیم. چطور؟ ابتدا تمام این توابع را export می کنیم تا در فایل های دیگر به آن ها دسترسی داشته باشیم. در مرحله بعدی به یک فایل دیگر رفته و route هایمان را در آنجا تعریف می کنیم و این توابع را به آن پاس می دهیم. انجام این کار با دستور module.exports ممکن است. من این کار را در انتهای فایل blogs.js انجام می دهم:

// Demo data

let blogs = [

  {

    id: 1,

    title: "This is an experiment from fastify-roxo project",

  },

  {

    id: 2,

    title: "Fastify is pretty cool",

  },

  {

    id: 3,

    title: "Just another blog, yea!",

  },

];




// Handlers

const getAllBlogs = async (req, reply) => {

  return blogs;

};




const getBlog = async (req, reply) => {

  const id = Number(req.params.id); // blog ID

  const blog = blogs.find(blog => blog.id === id);

  return blog;

};




const addBlog = async (req, reply) => {

  const id = blogs.length + 1; // generate new ID

  const newBlog = {

    id,

    title: req.body.title,

  };




  blogs.push(newBlog);

  return newBlog;

};




const updateBlog = async (req, reply) => {

  const id = Number(req.params.id);

  blogs = blogs.map(blog => {

    if (blog.id === id) {

      return {

        id,

        title: req.body.title,

      };

    }

  });




  return {

    id,

    title: req.body.title,

  };

};




const deleteBlog = async (req, reply) => {

  const id = Number(req.params.id);




  blogs = blogs.filter(blog => blog.id !== id);

  return { msg: `Blog with ID ${id} is deleted` };

};




module.exports = {

  getAllBlogs,

  getBlog,

  addBlog,

  updateBlog,

  deleteBlog,

};

همانطور که می بینید تمام این توابع توسط module.exports و به سادگی export شده اند.

تعریف route های برنامه

من می خواهم برای مسیر های برنامه از یک فایل جداگانه استفاده کنم بنابراین در پوشه اصلی برنامه (در کنار فایل index.js) یک پوشه جدید به نام routes ایجاد کنید. ما در این پوشه، فایلی به نام blogs.js ایجاد می کنیم. یکی از قابلیت های عالی fastify این است که به ما اجازه می دهد آرایه ای از مسیر های برنامه خودمان را تعریف کنیم بنابراین تمام مسیر ها را در همین آرایه و به شکل زیر تعریف می کنیم:

const blogController = require("../controller/blogs");




const routes = [

  {

    method: "GET",

    url: "/api/blogs",

    handler: blogController.getAllBlogs,

  },

  {

    method: "GET",

    url: "/api/blogs/:id",

    handler: blogController.getBlog,

  },

  {

    method: "POST",

    url: "/api/blogs",

    handler: blogController.addBlog,

  },

  {

    method: "PUT",

    url: "/api/blogs/:id",

    handler: blogController.updateBlog,

  },

  {

    method: "DELETE",

    url: "/api/blogs/:id",

    handler: blogController.deleteBlog,

  },

];

module.exports = routes;

دقت کنید که در ابتدا route handler های خود را require کرده ایم تا به توابع درون آن دسترسی داشته باشیم. هر شیء ای که به آرایه routes پاس داده ایم باید سه خصوصیت داشته باشد:

  • method: این بخش متد خاص تعامل با route را مشخص می کند. هر route می تواند یک یا چند نوع متد را قبول کند.
  • url: این مقدار همان route شما یا مسیر برنامه است. در بخش های قبلی توضیح دادم که این ها مسیر های مورد نیاز ما هستند.
  • handler: این همان route handler است. یعنی باید تابعی که مسئول مدیریت این مسیر است را مشخص کنید.

تا اینجا فقط یک آرایه ساده را تعریف کرده ایم اما هنوز آن را به عنوان مسیر های برنامه مان ثبت نکرده ایم. برای این کار به همان فایل اصلی برنامه (index.js) در پوشه اصلی برمی گردیم و این کار را انجام می دهیم:

// Require the framework and instantiate it

const app = require('fastify')({

    logger: true

})




// Declare a route

app.get('/', function (req, reply) {

    reply.send({ hello: 'world' })

})




// Register routes to handle blog posts

const blogRoutes = require('./routes/blogs')

blogRoutes.forEach((route, index) => {

    app.route(route)

})




// Run the server!

app.listen(3000, (err, address) => {

    if (err) {

        app.log.error(err)

        process.exit(1)

    }

    app.log.info(`server listening on ${address}`)

})

همانطور که می بینید ما بین تک تک اعضای آرایه routes گردش کرده ایم و در هر گردش یکی از مسیر ها را به متد app.route داده ایم. با این کار تمام مسیر های ما در برنامه ثبت می شوند. البته شما می توانستید به جای این کار مسیر ها را به صورت دستی و تک به تک نیز تعریف کنید اما من این روش را می پسندم.

حالا برای تست برنامه باید سرور را دوباره از ترمینال اجرا کنید. سپس به Insomnia رفته و یک درخواست GET را به آدرس http://localhost:3000/api/blogs/1 ارسال کنید. اگر همه چیز را به درستی انجام داده باشید، نتیجه زیر را می گیرید:

{

  "id": 1,

  "title": "This is an experiment from fastify-roxo project"

}

بنابراین همه چیز به درستی کار می کند. در ضمن در صورتی که دوست ندارید قسمت api در مسیر هایتان وجود داشته باشد می توانید به راحتی آن را از فایل blogs.js در پوشه routes حذف کنید:

const blogController = require("../controller/blogs");




const routes = [

  {

    method: "GET",

    url: "/blogs",

    handler: blogController.getAllBlogs,

  },

  {

    method: "GET",

    url: "/blogs/:id",

    handler: blogController.getBlog,

  },

  {

    method: "POST",

    url: "/blogs",

    handler: blogController.addBlog,

  },

  {

    method: "PUT",

    url: "/blogs/:id",

    handler: blogController.updateBlog,

  },

  {

    method: "DELETE",

    url: "/blogs/:id",

    handler: blogController.deleteBlog,

  },

];

module.exports = routes;

حالا می توانید درخواست خود را به http://localhost:3000/blogs/1 ارسال کنید. من این روش را ترجیح می دهم بنابراین آن را به همین شکل نگه می دارم.

اعتبارسنجی route ها

منظور من از اعتبارسنجی، اعتبارسنجی های عادی نیست که برای تمام برنامه ها انجام می دهیم بلکه منظور من schema validation است که نوع خاصی از validation (اعتبارسنجی) محسوب می شود و مربوط به کلیت داده ها است. ما می توانیم با پاس دادن قوانینی خاص به کلید schema از مسیر های خودمان، نوعی از اعتبارسنجی را روی آن ها پیاده کنیم. در واقع schema شیء ای است که طرح کلی درخواست و پاسخ را از قبل مشخص می کند و باید حتما در قالب JSON باشد.

به طور مثال ما یک id را در مسیر blogs/:id دریافت می کنیم بنابراین id یک URL Parameter است. طبیعتا می دانیم که id باید از نوع رشته باشد چرا که مقادیر دریافت شده از req.params همیشه رشته هستند اما پاسخ ما باید شامل دو کلید id و title باشد به طوری که id در آن یک عدد و title یک رشته باشد. چرا؟ به دلیل اینکه ما کد ها را بدین شکل نوشتیم (به route handler آن مراجعه کنید). اگر بخواهیم این schema یا طرح را به مسیر خودمان اضافه کنیم به فایل blogs.js در پوشه routes می رویم و شیء جدید زیر را به آن اضافه می کنیم:

const getBlogValidation = {

        params: {

            id: { type: 'string' }

        },

        response: {

            200: {

                type: 'object',

                properties: {

                    id: { type: 'integer' },

                    title: { type: 'string' }

                }

            }

        }

}

قسمت params نوع پارامتر های درخواست را مشخص می کند که ما در اینجا فقط یک پارامتر داشتیم و آن هم id بود. قسمت response نیز ساختار پاسخ ما را تعیین می کند. من فقط ساختار پاسخ در حالت ۲۰۰ (موفقیت آمیز بودن درخواست) را مشخص کرده ام و با دیگر حالت ها کاری ندارم. نوع درخواست ما طبیعتا یک شیء است و خصوصیت آن نیز id و title خواهند بود. در مرحله بعدی باید این شیء را با کلید schema به مسیر مورد نظرمان پاس بدهیم:

// بقیه کد ها

  {

    method: "GET",

    url: "/blogs/:id",

    schema: getBlogValidation,

    handler: blogController.getBlog,

  },

// بقیه کد ها

حالا یک schema validation را به مسیر ثبت پست جدید نیز اضافه می کنیم تا مطمئن شویم درخواست ارسال شده دارای خصوصیتی به نام title است:

const addBlogValidation = {

    body: {

        type: 'object',

        required: [

            'title'

        ],

        properties: {

            title: { type: 'string' }

        }

    },

    response: {

        200: {

            type: 'object',

            properties: {

                id: { type: 'integer' },

                title: { type: 'string' }

            }

        }

    }

}

مثل قبل body مشخص کننده بدنه درخواست است اما بای توجه داشته باشید که قسمت properties فقط توصیف کننده title است (رشته ای باشد یا عددی باشد یا ...) و الزامی در وجود آن ایجاد نمی کند. به همین خاطر اگر بخواهیم بگوییم که حضور فیلد title الزامی است باید آن را در بخش required (مقادیر الزامی) قرار داده ایم. response نیز مثل همیشه ساختار پاسخمان را توصیف می کند. در مرحله بعدی مسیر ثبت پست جدید را پیدا کرده و به شکل زیر عمل می کنیم:

// بقیه کد ها

  {

    method: "POST",

    url: "/blogs",

    schema: addBlogValidation,

    handler: blogController.addBlog,

  },

// بقیه کد ها

با این حساب تمام محتویات فایل blogs.js در پوشه routes به شکل زیر خواهد بود:

const blogController = require("../controller/blogs");




const getBlogValidation = {

  params: {

    id: { type: "string" },

  },

  response: {

    200: {

      type: "object",

      properties: {

        id: { type: "integer" },

        title: { type: "string" },

      },

    },

  },

};




const addBlogValidation = {

  body: {

    type: "object",

    required: ["title"],

    properties: {

      title: { type: "string" },

    },

  },

  response: {

    200: {

      type: "object",

      properties: {

        id: { type: "integer" },

        title: { type: "string" },

      },

    },

  },

};




const routes = [

  {

    method: "GET",

    url: "/blogs",

    handler: blogController.getAllBlogs,

  },

  {

    method: "GET",

    url: "/blogs/:id",

    schema: getBlogValidation,

    handler: blogController.getBlog,

  },

  {

    method: "POST",

    url: "/blogs",

    schema: addBlogValidation,

    handler: blogController.addBlog,

  },

  {

    method: "PUT",

    url: "/blogs/:id",

    handler: blogController.updateBlog,

  },

  {

    method: "DELETE",

    url: "/blogs/:id",

    handler: blogController.deleteBlog,

  },

];

module.exports = routes;

حالا بیایید کد هایمان را تست کنیم. ابتدا بدون ایجاد مشکل پستی که id شماره ۳ را دارد دریافت می کنیم. برای این کار باید یک درخواست GET به آدرس http://localhost:3000/blogs/3 ارسال کنیم. نتیجه باید بدون خطا و به شکل زیر باشد:

{

  "id": 3,

  "title": "Just another blog, yea!"

}

تا اینجا چیز عجیبی نداریم اما بیایید به فایل blogs.js در پوشه routes برگردیم و این بار نوع id را از رشته (string) به شیء (object) تغییر بدهیم:

const getBlogValidation = {

  params: {

    id: { type: "object" },

  },

  response: {

    200: {

      type: "object",

      properties: {

        id: { type: "integer" },

        title: { type: "string" },

      },

    },

  },

};

پس از ایجاد این تغییر طبیعتا باید سرور را ریستارت کنید و سپس یک درخواست GET دیگر را به http://localhost:3000/blogs/3 ارسال نمایید. این بار باید یک خطا از نوع خطای 400 (bad request) دریافت کنید و نتیجه زیر برایتان برگردانده شود:

{

  "statusCode": 400,

  "error": "Bad Request",

  "message": "params.id should be object"

}

به همین راحتی می توانیم به تیم توسعه front-end کمک کنیم تا اشتباهات خود را اصلاح کنند. خطای بالا می گوید params.id should be object که یعنی params.id باید از نوع object باشد. به این قابلیت کاربردی schema validation می گویند. حالا می توانید نوع داده id را دوباره به string برگردانید.

نحوه استفاده از پلاگین های Fastify

اگر به این صفحه از وب سایت fastify بروید برایتان توضیح داده شده است که برای بارگذاری پلاگین ها پیشنهاد اکید می شود که از ترتیب زیر پیروی کنید:

└── plugins (from the Fastify ecosystem)

└── your plugins (your custom plugins)

└── decorators

└── hooks

└── your services

یعنی ابتدا پلاگین هایی که در اکوسیستم fastify وجود دارد (پلاگین های core و community) را بارگذاری کنید و سپس پلاگین هایی که خودتان نوشته اید را بارگذاری کنید، سپس decorator ها و سپس hook ها و در نهایت سرویس هایی که برای برنامه نوشته اید. این ترتیب برای پرهیز از ایجاد تصادم بین پلاگین ها حیاتی است بنابراین سعی کنید همیشه از این قانون پیروی کنید.

در fastify پلاگین های مختلفی وجود دارند. به طور مثال پلاگینی به نام fastify-env وجود دارد که به شما اجازه می دهد environment variable ها یا متغیر های محیطی را بارگذاری کرده و به آن ها مقدار پیش فرض بدهید. همچنین پلاگینی به نام fastify-routes وجود دارد که تمام route های شما را چاپ می کند تا بدانید دقیقا چه route هایی دارید. بیایید برای مثال از پلاگین دوم استفاده کنیم. من می خواهم از fastify-routes استفاده کنم بنابراین باید ابتدا آن را نصب کنیم:

npm install --save fastify-routes

در مرحله بعدی به فایل اصلی پروژه، یعنی index.js، می رویم و از این پلاگین به شکل زیر استفاده می کنیم:

// Require the framework and instantiate it

const app = require("fastify")({

  logger: true,

});




// Fastify-routes plugin instantiation

app.register(require("fastify-routes")); // Add and register plugin




// Declare a route

app.get("/", function (req, reply) {

  reply.send({ hello: "world" });

});




// Register routes to handle blog posts

const blogRoutes = require("./routes/blogs");

blogRoutes.forEach((route, index) => {

  app.route(route);

});




// Run the server!

app.listen(3000, (err, address) => {

  if (err) {

    app.log.error(err);

    process.exit(1);

  }

  app.log.info(`server listening on ${address}`);

  console.log(app.routes);

});

همانطور که می بینید من پلاگین خودم را ابتدا وارد پروژه کرده ام. برای ثبت یک پلاگین در fastify و استفاده از آن همیشه از متد register استفاده می کنیم و من هم در اینجا app.register را صدا زده ام و این پلاگین را به آن پاس داده ام. من در اینجا پلاگین را مستقیما با require کردن پاس داده ام اما شما می توانید ابتدا آن را در یک متغیر قرار بدهید و سپس متغیر را به register پاس بدهید. با این کار پلاگین ما در fastify ثبت می شود. کار پلاگین fastify-routes چیست؟ اینکه یک خصوصیت به نام routes را به برنامه شما (شیء app) اضافه می کند. این خصوصیت لیست کاملی از مسیر های مختلف شما است. برای مشاهده این مسیر ها مثل من می توانید از دستور console.log در انتهای فایل استفاده کنید (بعد از اینکه که سرور اجرا شده است). با اجرای کد بالا نتیجه ای بدین شکل دریافت می کنید:

{"level":30,"time":1616787422399,"pid":35182,"hostname":"HP","msg":"Server listening at http://127.0.0.1:3000"}

{"level":30,"time":1616787422400,"pid":35182,"hostname":"HP","msg":"server listening on http://127.0.0.1:3000"}

Map(3) {

  '/' => {

    get: {

      method: 'GET',

      schema: undefined,

      url: '/',

      logLevel: '',

      prefix: '',

      bodyLimit: undefined,

      handler: [Function (anonymous)]

    }

  },

  '/blogs' => {

    get: {

      method: 'GET',

      schema: undefined,

      url: '/blogs',

      logLevel: '',

      prefix: '',

      bodyLimit: undefined,

      handler: [AsyncFunction: getAllBlogs]

    },

    post: {

      method: 'POST',

      schema: [Object],

      url: '/blogs',

      logLevel: '',

      prefix: '',

      bodyLimit: undefined,

      handler: [AsyncFunction: addBlog]

    }

  },

  '/blogs/:id' => {

    get: {

      method: 'GET',

      schema: [Object],

      url: '/blogs/:id',

      logLevel: '',

      prefix: '',

      bodyLimit: undefined,

      handler: [AsyncFunction: getBlog]

    },

    put: {

      method: 'PUT',

      schema: undefined,

      url: '/blogs/:id',

      logLevel: '',

      prefix: '',

      bodyLimit: undefined,

      handler: [AsyncFunction: updateBlog]

    },

    delete: {

      method: 'DELETE',

      schema: undefined,

      url: '/blogs/:id',

      logLevel: '',

      prefix: '',

      bodyLimit: undefined,

      handler: [AsyncFunction: deleteBlog]

    }

  }

}

همانطور که از این نتیجه می بینید ما سه مسیر اصلی داریم: مسیر / و مسیر blogs/ و مسیر blogs/:id/ و هر کدام از آن ها یک یا چند متد (GET و POST و ...) دارند.  حتما شما هم متوجه می شوید که در برنامه های بسیار بزرگ این پلاگین چه کمک بزرگی به ما خواهد کرد. البته در برنامه های بزرگ به جای log کردن مسیر ها آن ها را درون یک فایل قرار می دهیم و می توانیم به صورت یکجا اطلاعاتی کلی را راجع به تمام مسیر هایمان داشته باشیم.

فرآیندی که در این قسمت طی کردیم مخصوص پلاگین fastify-routes بود اما پلاگین های دیگری نیز وجود دارند که هر کدام قابلیت های خاص خودشان را دارند و نحوه نصب و register آن ها نیز کمی متفاوت است بنابراین قبل از نصب هر پلاگین حتما به صفحه اصلی آن مراجعه کرده و روش نصب را مطالعه کنید.

من برای شلوغ نشدن ترمینال، این پلاگین را از برنامه حذف می کنم.

کار با hook ها

در fastify قابلیت جالبی به نام hook ها وجود دارد که بسیار کاربردی هستند. hook ها متد های خاصی هستند که به شما اجازه می دهند به رویداد (event) های خاص در برنامه خود یا رویداد های خاص در چرخه درخواست و پاسخ سرور گوش کرده و در صورت اتفاق افتادن آن ها، کار خاصی را انجام بدهید. برای استفاده از hook ها یک قانون اصلی داریم: hook ها باید همیشه قبل از event یا رویداد مورد نظر تعریف شده باشند در غیر این صورت هیچ اتفاقی نمی افتد.

در fastify انواع و اقسام رویداد های مختلف را داریم که در documentation رسمی آن توضیح داده شده اند. چند مورد از آن ها عبارت اند از:

  • onRequest: زمانی که درخواستی به سرور ارسال می شود.
  • preParsing: زمانی که در مرحله قبل از تجزیه و پردازش درخواست قرار داریم.
  • preValidation: زمانی که در مرحله قبل از اعتبارسنجی قرار داریم.
  • preHandler: زمانی که در مرحله قبل از فعال شدن route handler هستیم.
  • و الی آخر...

شما می توانید با مراجعه به documentation رسمی آن، تمام این رویداد های مختلف را مشاهده کنید.

برای اینکه خودمان یک مثال ساده از این hook ها را انجام بدهیم به فایل index.js رفته و قبل از تعریف کردن route اصلی (آدرس /) یک hook تعریف می کنیم:

// Require the framework and instantiate it

const app = require("fastify")({

  logger: true,

});




// hooks

app.addHook("onRoute", routeOptions => {

  console.log(`>>> New HOOK: Registered route: ${routeOptions.url}`);

});




// Declare a route

app.get("/", function (req, reply) {

  reply.send({ hello: "world" });

});




// Register routes to handle blog posts

const blogRoutes = require("./routes/blogs");

blogRoutes.forEach((route, index) => {

  app.route(route);

});




// Run the server!

app.listen(3000, (err, address) => {

  if (err) {

    app.log.error(err);

    process.exit(1);

  }

  app.log.info(`server listening on ${address}`);

});

همانطور که در کد بالا می بینید من با استفاده از متد app.addHook می توانم یک hook جدید را تعریف کنم. آرگومان اولی که پاس داده ایم نوع رویداد مورد نظر ما است و من onRoute را انتخاب کرده ام. رویداد onRoute زمانی اجرا می شود که یک route (مسیر) جدید در fastify ثبت می شود. آرگومان دوم همان تابع یا hook مورد نظر شما است که باید کار خاصی انجام دهد. اگر از رویداد onRoute استفاده نمایید یک شیء به نام routeOptions برای hook شما ارسال می شود که شامل اطلاعاتی از route ثبت شده است. مثلا خصوصیت url در این شیء به URL یا آدرس آن route اشاره می کند و من برای سادگی کار فقط این مقدار را console.log کرده ام. حالا اگر سرور خود را دوباره اجرا کنید چنین نتیجه ای را در ترمینال می گیرید:

>>> New HOOK: Registered route: /

>>> New HOOK: Registered route: /blogs

>>> New HOOK: Registered route: /blogs/:id

>>> New HOOK: Registered route: /blogs

>>> New HOOK: Registered route: /blogs/:id

>>> New HOOK: Registered route: /blogs/:id

{"level":30,"time":1616824706302,"pid":9746,"hostname":"HP","msg":"Server listening at http://127.0.0.1:3000"}

{"level":30,"time":1616824706303,"pid":9746,"hostname":"HP","msg":"server listening on http://127.0.0.1:3000"}

همانطور که می بینید تک تک مسیر های ما در این بخش چاپ شده اند! در ضمن توجه داشته باشید که ترتیب ثبت route ها قبل از اجرا شدن سرور است بنابراین log شدن آن ها قبل از log شدن اطلاعات سرور می باشد. در وب سایت رسمی fastify لیستی از انواع خصوصیت در شیء routeOptions لیست شده است:

fastify.addHook('onRoute', (routeOptions) => {

  //Some code

  routeOptions.method

  routeOptions.schema

  routeOptions.url // the complete URL of the route, it will inclued the prefix if any

  routeOptions.path // `url` alias

  routeOptions.routePath // the URL of the route without the prefix

  routeOptions.bodyLimit

  routeOptions.logLevel

  routeOptions.logSerializers

  routeOptions.prefix

})

شما می توانید با log کردن هر کدام از این مقادیر نتیجه را مشاهده کنید.

decorator چیست؟

decorator ها به شما اجازه می دهند اشیاء اصلی Fastify (مثل نمونه ساخته شده از پکیج fastify که در برنامه ما app نام دارد یا شیء req یا شیء reply و غیره) را ویرایش کنید. معمولا زمانی از decorator ها استفاده می شود که بخواهیم خصوصیت خاصی را به شیء هسته ای fastify اضافه کنیم. البته در نظر داشته باشید که decorator ها به صورت همگام یا synchronous اجرا می شوند و اگر بخواهید آن ها را به صورت ناهمگام اجرا کنید باید از fastify-plugin کمک بگیرید.

شاید بپرسید چرا به شکل عادی یک خصوصیت را به اشیاء هسته ای برنامه اضافه نکنیم؟ ما می دانیم که در جاوا اسکریپت اضافه کردن خصوصیات به یک شیء ساخته شده کاملا ممکن است. توجه داشته باشید که برای انجام این کار ابتدا باید شیء ساخته شده باشد اما ما نباید شکل شیء اصلی برنامه را در حین عملیات های سرور و پس از ساخته شدنش تغییر بدهیم چرا که می توانیم باعث ایجاد مشکلات فراوانی بشویم. به طور مثال، کد زیر یک نمونه بسیار بد از استفاده از decorator ها است:

// مثال بسیار بد




// Attach a user property to the incoming request before the request

// handler is invoked.

fastify.addHook('preHandler', function (req, reply, done) {

  req.user = 'Bob Dylan'

  done()

})




// Use the attached user property in the request handler.

fastify.get('/', function (req, reply) {

  reply.send(`Hello, ${req.user}`)

})

من در این مثال سعی کرده ام که خصوصیتی به نام user را به شیء req برنامه اضافه کنم و از روش عادی جاوا اسکریپتی استفاده کرده ام. چرا این مثال بد است؟ به دلیل در این کد شیء fastify را در حالی mutate کرده ایم که در میان برنامه هستیم. مثال صحیح انجام این کار با استفاده از decorator ها است:

// Decorate request with a 'user' property

fastify.decorateRequest('user', '')




// Update our property

fastify.addHook('preHandler', (req, reply, done) => {

  req.user = 'Bob Dylan'

  done()

})

// And finally access it

fastify.get('/', (req, reply) => {

  reply.send(`Hello, ${req.user}!`)

})

متد decorateRequest در fastify به شما اجازه می دهد که شیء req در برنامه را قبل از ساخته شدن، ویرایش نمایید.

middleware های fastify

در سال ۲۰۲۰ نسخه سوم از فریم ورک fastify ارائه شد و از این نسخه به بعد middleware ها به صورت پیش فرض در fastify پشتیبانی نمی شوند اما پلاگینی به نام middie وجود دارد که middleware ها را به fastify اضافه می کند.

برای استفاده از middie ابتدا باید این پلاگین را نصب کنید:

npm install middie

در مرحله بعدی می توانید مانند middleware های express.js از app.use برای اضافه کردن middleware ها استفاده نمایید:

await fastify.register(require('middie'))

fastify.use(require('cors')())

البته پلاگین دیگری به نام fastify-express نیز وجود دارد که middleware ها را برایتان اضافه می کند و نحوه استفاده از آن دقیقا مانند middie است:

await fastify.register(require('fastify-express'))

fastify.use(require('cors')())

fastify.use(require('dns-prefetch-control')())

fastify.use(require('frameguard')())

fastify.use(require('hsts')())

fastify.use(require('ienoopen')())

fastify.use(require('x-xss-protection')())

همانطور که می بینید تفاوتی بین این دو پلاگین وجود ندارد.

امیدوارم این مقاله به درک شما از فریم ورک Fastify و زیبایی های آن کمک کرده باشد. توجه داشته باشید که این مقاله فقط به عنوان آشنایی اولیه بود و اگر می خواهید fastify را یاد بگیرید باید حتما documentation آن را مطالعه کنید چرا که هنوز ده ها قابلیت وجود دارد که من در مورد آن ها صحبتی نکرده ام.

نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.