الدليل الشامل لبناء واجهة برمجة تطبيقات (API) باستخدام TypeScript و AWS

دقائق القراءة: 12

مقدمة: بناء واجهة برمجة تطبيقات حديثة وفعالة

في عالم التطوير المتسارع، أصبح بناء واجهات برمجة التطبيقات (APIs) الفعالة والقابلة للتطوير أمرًا ضروريًا. يقدم هذا الدليل الشامل منهجًا عمليًا لبناء واجهة برمجة تطبيقات قوية باستخدام TypeScript و Serverless Framework على منصة AWS. سنتعمق في كيفية الاستفادة من هذه التقنيات لإنشاء واجهة برمجة تطبيقات لترجمة النصوص تلقائيًا، مما يضمن لك محتوى حصريًا وقيمة مضافة لمشروعك.

البدء: تهيئة بيئة العمل

لبدء هذه العملية، نحتاج إلى التأكد من توفر بعض المتطلبات الأساسية وتكوين بيئة العمل بشكل صحيح.

المتطلبات الأساسية

قبل الشروع في بناء مشروعنا، تأكد من تثبيت Serverless Framework وتكوين ملف تعريف AWS على جهاز الكمبيوتر الخاص بك. هذه الخطوات حاسمة لضمان إمكانية نشر وتفاعل مشروعك مع خدمات AWS. إذا لم تكن قد قمت بذلك، فمن الضروري إعداد هذه المكونات أولاً.

تهيئة المشروع باستخدام Serverless Framework

الآن، حان وقت إنشاء مشروع Serverless وواجهة برمجة التطبيقات (API) الخاصة بنا. ابدأ بفتح الطرفية (terminal) وقم بتشغيل الأمر التالي لإنشاء مستودع جديد. تأكد من استبدال {YOUR FOLDER NAME} بالاسم الذي تختاره لمجلد مشروعك:

serverless create --template aws-nodejs-typescript --path {YOUR FOLDER NAME}

سيؤدي هذا الأمر إلى إنشاء مشروع Serverless أساسي جدًا مع دعم TypeScript. عند فتح هذا المجلد الجديد باستخدام VS Code، يمكنك استكشاف الملفات التي يوفرها القالب.

لقطة شاشة لمجلد مشروع Serverless جديد في VS Code يظهر ملفات القالب

فهم ملفات القالب الرئيسية

الملفات الرئيسية التي سنركز عليها هي serverless.ts و handler.ts.

  • serverless.ts: هذا الملف هو قلب التكوين لعملية النشر. يخبر هذا الملف إطار عمل Serverless باسم المشروع، ولغة تشغيل الكود (runtime language)، وقائمة الدوال (functions)، وبعض خيارات التكوين الأخرى. في كل مرة نرغب فيها بتغيير بنية مشروعنا، سيكون هذا هو الملف الذي نعمل عليه.
  • handler.ts: هنا نجد الكود النموذجي لدالة Lambda الذي وفره لنا القالب. إنه بسيط للغاية ويعيد استجابة API Gateway مع رسالة وحدث الإدخال (input event). سنستخدم هذا لاحقًا كنقطة انطلاق لواجهة برمجة التطبيقات الخاصة بنا.

إنشاء دالة Lambda خاصة بك

بعد أن تعرفنا على ما يوفره لنا القالب، حان الوقت لإضافة دالة Lambda ونقطة نهاية (API endpoint) خاصة بنا.

هيكلة كود Lambda

للبدء، سنقوم بإنشاء مجلد جديد ليحتوي على جميع أكواد Lambda الخاصة بنا ونسميه lambdas. يساعد هذا في تنظيم المشروع، خاصة عندما تبدأ في الحصول على عدة دوال Lambda مختلفة في مشروع واحد. في هذا المجلد الجديد، سنقوم بإنشاء دالة Lambda الجديدة الخاصة بنا، وسنطلق عليها اسم getCityInfo.ts. عند فتح هذا الملف، يمكننا البدء في كتابة الكود الخاص بنا. يمكننا البدء بنسخ جميع أكواد handler.ts كنقطة انطلاق.

استخلاص معاملات المسار وتغيير اسم الدالة

أول شيء سنفعله هو تغيير اسم الدالة إلى handler. هذا تفضيل شخصي، ولكني أفضّل تسمية الدالة التي تتعامل مع الحدث بـ handler. في السطر الأول من هذه الدالة، نحتاج إلى إضافة بعض الأكواد للحصول على اسم المدينة التي يطلبها المستخدم. يمكننا الحصول على هذا من مسار URL باستخدام pathParameters.

const city = event.pathParameters?.city;

شرح السلسلة الاختيارية (Optional Chaining)

قد تلاحظ استخدام ?. في هذا الإعلان. هذه هي ميزة السلسلة الاختيارية (Optional Chaining) وهي ميزة رائعة حقًا. تعني أنه إذا كان معامل المسار (path parameter) موجودًا (truthy)، فاحصل على معامل المدينة (city parameter)، وإلا فقم بإرجاع undefined. هذا يعني أنه إذا لم يكن pathParameters كائنًا، فلن تحصل على الخطأ cannot read property _city_ of undefined الذي يتسبب في حدوث خطأ في وقت تشغيل Node.

تحديد هيكل البيانات باستخدام الواجهات (Interfaces)

الآن بعد أن أصبح لدينا اسم المدينة، نحتاج إلى التحقق من أن المدينة صالحة وأن لدينا بيانات لتلك المدينة. لهذا، نحتاج إلى بعض البيانات. يمكننا استخدام الكود أدناه ولصقه في أسفل الملف.

interface CityData {
  name: string;
  state: string;
  description: string;
  mayor: string;
  population: number;
  zipCodes?: string;
}

const cityData: { [key: string]: CityData } = {
  newyork: {
    name: 'New York',
    state: 'New York',
    description: 'New York City comprises 5 boroughs sitting where the Hudson River meets the Atlantic Ocean. At its core is Manhattan, a densely populated borough that’s among the world’s major commercial, financial and cultural centers. Its iconic sites include skyscrapers such as the Empire State Building and sprawling Central Park. Broadway theater is staged in neon-lit Times Square.',
    mayor: 'Bill de Blasio',
    population: 8399000,
    zipCodes: '100xx–104xx, 11004–05, 111xx–114xx, 116xx',
  },
  washington: {
    name: 'Washington',
    state: 'District of Columbia',
    description: `DescriptionWashington, DC, the U.S. capital, is a compact city on the Potomac River, bordering the states of Maryland and Virginia. It’s defined by imposing neoclassical monuments and buildings – including the iconic ones that house the federal government’s 3 branches: the Capitol, White House and Supreme Court. It's also home to iconic museums and performing-arts venues such as the Kennedy Center.`,
    mayor: 'Muriel Bowser',
    population: 705549,
  },
  seattle: {
    name: 'Seattle',
    state: 'Washington',
    description: `DescriptionSeattle, a city on Puget Sound in the Pacific Northwest, is surrounded by water, mountains and evergreen forests, and contains thousands of acres of parkland. Washington State’s largest city, it’s home to a large tech industry, with Microsoft and Amazon headquartered in its metropolitan area. The futuristic Space Needle, a 1962 World’s Fair legacy, is its most iconic landmark.`,
    mayor: 'Jenny Durkan',
    population: 744955,
  },
};

الفرق بين هذا و JavaScript هو أنه يمكننا إنشاء واجهة (interface) لإخبار النظام بهيكل البيانات الذي يجب أن يكون عليه. قد يبدو هذا كعمل إضافي في البداية ولكنه سيساعد في جعل كل شيء أسهل لاحقًا. داخل الواجهة الخاصة بنا، نحدد مفاتيح كائن المدينة؛ بعضها من نوع string، وواحد من نوع number، ثم zipCodes هي خاصية اختيارية (optional property). هذا يعني أنها قد تكون موجودة ولكن لا يجب أن تكون كذلك.

إذا أردنا اختبار الواجهة الخاصة بنا، يمكننا محاولة إضافة خاصية جديدة إلى أي من المدن في بيانات المدينة الخاصة بنا. سيخبرك TypeScript على الفور أن الخاصية الجديدة غير موجودة في الواجهة. إذا قمت بحذف إحدى الخصائص المطلوبة، فسيشتكي TypeScript أيضًا. هذا يضمن أن لديك دائمًا البيانات الصحيحة وأن الكائنات تبدو دائمًا كما هو متوقع تمامًا.

تطبيق استجابات واجهة برمجة التطبيقات (API Responses)

الآن بعد أن أصبح لدينا البيانات، يمكننا التحقق مما إذا كان المستخدم قد أرسل طلب المدينة الصحيح.

if (!city || !cityData[city]) {
  // Handle error
}

إذا كانت هذه العبارة صحيحة، فهذا يعني أن المستخدم قد ارتكب خطأ ما، وبالتالي نحتاج إلى إرجاع استجابة 400. يمكننا فقط كتابة الكود يدويًا هنا، ولكننا سنقوم بإنشاء كائن apiResponses جديد مع دوال (methods) لبعض أكواد استجابة API المحتملة.

const apiResponses = {
  _200: (
    body: { [key: string]: any }
  ) => {
    return {
      statusCode: 200,
      body: JSON.stringify(body, null, 2),
    };
  },
  _400: (
    body: { [key: string]: any }
  ) => {
    return {
      statusCode: 400,
      body: JSON.stringify(body, null, 2),
    };
  },
};

هذا يجعل إعادة الاستخدام أسهل بكثير لاحقًا في الملف. يجب أن تلاحظ أيضًا أن لدينا خاصية واحدة body: { [key: string]: any }. هذا يشير إلى أن هذه الدالة لها خاصية واحدة من نوع body والتي يجب أن تكون كائنًا. يمكن أن يحتوي هذا الكائن على مفاتيح لها قيمة من أي نوع. نظرًا لأننا نعلم أن body سيكون دائمًا سلسلة نصية، يمكننا استخدام JSON.stringify للتأكد من أننا نعيد جسمًا نصيًا.

إذا أضفنا هذه الدالة إلى handler الخاص بنا، فسنحصل على هذا:

export const handler: APIGatewayProxyHandler = async (event, _context) => {
  const city = event.pathParameters?.city;

  if (!city || !cityData[city]) {
    return apiResponses._400({ message: 'missing city or no data for that city' });
  }

  return apiResponses._200(cityData[city]);
};

إذا لم يقم المستخدم بتمرير مدينة أو مرر مدينة لا توجد لدينا بيانات لها، فإننا نعيد استجابة 400 مع رسالة خطأ. إذا كانت البيانات موجودة، فإننا نعيد استجابة 200 مع جسم يحتوي على البيانات.

إضافة واجهة برمجة تطبيقات ترجمة جديدة

في القسم السابق، قمنا بإعداد مستودع API TypeScript الخاص بنا وأنشأنا دالة Lambda تستخدم بيانات مبرمجة ثابتة. سيعلمك هذا الجزء كيفية استخدام aws-sdk للتفاعل مباشرة مع خدمات AWS الأخرى لإنشاء واجهة برمجة تطبيقات قوية حقًا.

إعداد دالة Lambda للترجمة

للبدء، نحتاج إلى إضافة ملف جديد لواجهة برمجة تطبيقات الترجمة الخاصة بنا. أنشئ ملفًا جديدًا ضمن مجلد lambdas باسم translate.ts. يمكننا البدء بهذا الملف ببعض الأكواد الأساسية. هذا هو الكود الأولي لدالة Lambda API TypeScript.

import { APIGatewayProxyHandler } from 'aws-lambda';
import 'source-map-support/register';

export const handler: APIGatewayProxyHandler = async (event) => {
  // Translation logic will go here
};

تحليل جسم الطلب (Parsing Request Body)

الآن نحتاج إلى الحصول على النص الذي يرغب المستخدم في ترجمته واللغة التي يريد الترجمة إليها. يمكننا الحصول على هذه البيانات من جسم الطلب (body of the request). شيء إضافي يجب علينا فعله هنا هو تحليل الجسم (parse the body). بشكل افتراضي، يقوم API Gateway بتحويل أي JSON يتم تمريره في الجسم إلى سلسلة نصية. يمكننا بعد ذلك استخراج text و language من الجسم.

const body = JSON.parse(event.body);
const { text, language } = body;

الآن نحتاج إلى التحقق مما إذا كان المستخدم قد مرر النص واللغة.

if (!text) {
  // return 400
}
if (!language) {
  // return 400
}

مركزية استجابات واجهة برمجة التطبيقات (Centralizing API Responses)

في الجزء الأخير، أنشأنا استجابة 400 كدالة في الملف. نظرًا لأننا سنستخدم استجابات API هذه عبر ملفات متعددة، فمن الجيد سحبها إلى ملف مشترك خاص بها. أنشئ مجلدًا جديدًا ضمن lambdas باسم common. هذا هو المكان الذي سنخزن فيه جميع الدوال المشتركة. في هذا المجلد، أنشئ ملفًا جديدًا باسم apiResponses.ts. سيقوم هذا الملف بتصدير الكائن apiResponses مع دوال _200 و _400 عليه. إذا كان عليك إرجاع أكواد استجابة أخرى، فيمكنك إضافتها إلى هذا الكائن.

const apiResponses = {
  _200: (
    body: { [key: string]: any }
  ) => {
    return {
      statusCode: 200,
      body: JSON.stringify(body, null, 2),
    };
  },
  _400: (
    body: { [key: string]: any }
  ) => {
    return {
      statusCode: 400,
      body: JSON.stringify(body, null, 2),
    };
  },
};

export default apiResponses;

يمكننا الآن استيراد هذا الكائن إلى الكود الخاص بنا واستخدام هذه الدوال المشتركة. في أعلى ملف translate.ts الخاص بنا، يمكننا الآن إضافة هذا السطر:

import apiResponses from './common/apiResponses';

وتحديث عمليات التحقق من text و language لاستدعاء دالة _400 على هذا الكائن:

if (!text) {
  return apiResponses._400({ message: 'missing text from the body' });
}

if (!language) {
  return apiResponses._400({ message: 'missing language from the body' });
}

بعد الانتهاء من ذلك، نعلم أن لدينا النص المراد ترجمته واللغة المراد الترجمة إليها، لذا يمكننا بدء عملية الترجمة. يعد استخدام aws-sdk دائمًا تقريبًا مهمة غير متزامنة (async task)، لذا سنقوم بتغليفه في كتلة try/catch لتسهيل معالجة الأخطاء.

try {
  // Translation logic
} catch (error) {
  // Error handling
}

دمج AWS SDK للترجمة

أول شيء نحتاج إلى فعله هو استيراد aws-sdk وإنشاء مثيل جديد لخدمة الترجمة (translate service). للقيام بذلك، نحتاج إلى تثبيت aws-sdk ثم استيراده. أولاً، قم بتشغيل npm install --save aws-sdk ثم أضف هذا الكود إلى أعلى ملف الترجمة الخاص بك:

import * as AWS from 'aws-sdk';

const translate = new AWS.Translate();

بهذا، يمكننا البدء في كتابة كود الترجمة الخاص بنا. سنبدأ بالسطر الذي يقوم بالترجمة أولاً. أضف هذا في قسم try.

const translatedMessage = await translate.translateText(translateParams).promise();

شيء قد لاحظه البعض منكم هو أننا نمرر translateParams دون تعريفه بعد. هذا لأننا لسنا متأكدين من نوعه بعد. لمعرفة ذلك، يمكننا استخدام أداة في VS Code تسمى go to definition. تتيح لنا هذه الأداة الانتقال إلى حيث يتم تعريف الدالة حتى نتمكن من معرفة نوع المعاملات. يمكنك إما النقر بزر الماوس الأيمن وتحديد go to definition أو الضغط على Ctrl والنقر على الدالة.

لقطة شاشة لـ VS Code تظهر خيار 'Go to Definition' لدالة translateText

كما ترى، تأخذ دالة translateText معاملًا من نوع Translate.Types.TranslateTextRequest. طريقة أخرى لمعرفة ذلك هي استخدام IntelliSense عن طريق تحريك مؤشر الماوس فوق دالة translateText. يجب أن ترى هذا، حيث يمكنك رؤية أن params: AWS.Translate.TranslateTextRequest:

لقطة شاشة لـ VS Code تظهر تلميح IntelliSense لدالة translateText مع نوع المعاملات

بهذا، يمكننا إنشاء معاملات الترجمة الخاصة بنا فوق طلب الترجمة الذي قمنا به سابقًا. يمكننا بعد ذلك ملؤها بناءً على النوع الذي نحدده. هذا يضمن أننا نمرر الحقول الصحيحة.

const translateParams: AWS.Translate.Types.TranslateTextRequest = {
  Text: text,
  SourceLanguageCode: 'en',
  TargetLanguageCode: language,
};

الآن بعد أن أصبح لدينا المعاملات ونقوم بتمريرها إلى دالة translate.translateText، يمكننا البدء في إنشاء استجابتنا. ستكون هذه مجرد استجابة 200 مع الرسالة المترجمة.

return apiResponses._200({ translatedMessage });

بعد الانتهاء من كل ذلك، يمكننا الانتقال إلى قسم catch. هنا، نريد فقط تسجيل الخطأ ثم إرجاع استجابة 400 من الملف المشترك.

console.log('error in the translation', error);
return apiResponses._400({ message: 'unable to translate the message' });

بعد الانتهاء من ذلك، انتهينا من كود Lambda الخاص بنا، لذا نحتاج إلى الانتقال إلى ملف serverless.ts لإضافة نقطة نهاية API الجديدة هذه ومنحها الأذونات التي تحتاجها.

تكوين Serverless لواجهة برمجة تطبيقات الترجمة

في ملف serverless.ts، يمكننا التمرير لأسفل إلى قسم functions. هنا نحتاج إلى إضافة دالة جديدة إلى الكائن.

translate: {
  handler: 'lambdas/translate.handler',
  events: [
    {
      http: {
        path: 'translate',
        method: 'POST',
        cors: true,
      },
    },
  ],
},

الفرق الرئيسي بين هذا ونقطة النهاية السابقة هو أن نقطة النهاية الآن هي طريقة POST. هذا يعني أنه إذا حاولت إجراء طلب GET إلى مسار URL هذا، فستحصل على استجابة خطأ.

الشيء الأخير الذي يجب فعله هو منح دوال Lambda الإذن لاستخدام خدمة الترجمة (Translate service). مع جميع خدمات AWS تقريبًا، ستحتاج إلى إضافة أذونات إضافية لتتمكن من استخدامها من داخل دالة Lambda. للقيام بذلك، نضيف حقلًا جديدًا إلى قسم provider يسمى iamRoleStatements. هذه مصفوفة من عبارات السماح (allow) أو الرفض (deny) لخدمات وموارد مختلفة.

iamRoleStatements: [
  {
    Effect: 'Allow',
    Action: ['translate:*'],
    Resource: '*',
  },
],

اختبار واجهة برمجة تطبيقات الترجمة

مع إضافة هذا، أصبح لدينا كل ما نحتاجه جاهزًا، لذا يمكننا تشغيل sls deploy لنشر واجهة برمجة التطبيقات الجديدة الخاصة بنا. بمجرد نشرها، يمكننا الحصول على URL الخاص بواجهة برمجة التطبيقات واستخدام أداة مثل Postman أو postwoman.io لإجراء طلب إلى URL هذا. نحتاج فقط إلى تمرير جسم (body) على النحو التالي:

{
  "text": "This is a test message for translation",
  "language": "fr"
}

وبعد ذلك يجب أن نحصل على استجابة 200 على النحو التالي:

{
  "translatedMessage": {
    "TranslatedText": "Ceci est un message de test pour la traduction",
    "SourceLanguageCode": "en",
    "TargetLanguageCode": "fr"
  }
}

ملخص

في هذا المقال، تعلمنا كيفية:

  • إعداد مستودع TypeScript جديد باستخدام الأمر serverless create --template aws-nodejs-typescript.
  • إضافة دالة Lambda خاصة بنا تعيد مجموعة من البيانات المبرمجة ثابتة.
  • إضافة دالة Lambda هذه كنقطة نهاية (API endpoint).
  • إضافة دالة Lambda أخرى ستقوم تلقائيًا بترجمة أي نص يتم تمريره إليها.
  • إضافة نقطة نهاية API ومنح دالة Lambda الأذونات التي تحتاجها للعمل.

إذا استمتعت بهذا المقال وترغب في معرفة المزيد عن Serverless و AWS، فإنني أوصي بمشاهدة مقاطع الفيديو الأكثر إثارة للاهتمام في قائمة التشغيل الخاصة بي حول Serverless و AWS.

الخلاصة التقنية

لقد أثبت هذا الدليل بوضوح كيف يمكن لدمج TypeScript و Serverless Framework على AWS أن يحدث ثورة في طريقة بناء واجهات برمجة التطبيقات. إن قوة TypeScript في توفير السلامة النوعية (type safety) تقلل الأخطاء وتزيد من قابلية صيانة الكود، بينما تتيح بنية Serverless بناء تطبيقات قابلة للتوسع (scalable) وفعالة من حيث التكلفة. القدرة على دمج خدمات AWS SDK مثل AWS Translate مباشرة في دوال Lambda تفتح آفاقًا واسعة لإنشاء ميزات غنية ومعقدة بسهولة. هذا النهج لا يسرع عملية التطوير فحسب، بل يضمن أيضًا مرونة عالية واستجابة لاحتياجات العمل المتغيرة، مما يجعله خيارًا مثاليًا للمشاريع الحديثة التي تتطلب أداءً عاليًا وكفاءة تشغيلية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *