ساخت APIS بدون سرور با سرور بدون سرور، Node.js، Typescript، DynamoDB و Lambda.

Node.js به پلتفرمی برای ساخت برنامه های سمت سرور با جاوا اسکریپت تبدیل شده است. اکوسیستم آن، که با ابزارهای اساسی مانند Express شروع می شود، به طور قابل توجهی تکامل یافته است و چارچوب ها و کتابخانه های متنوعی را ارائه می دهد که پارادایم های مختلف طراحی را ارائه می دهد. به طور سنتی، ساخت API شامل نوشتن کد برای پیکربندی و مدیریت سرورها بود.
با این حال، ظهور معماریهای بدون سرور، این الگو را تغییر داده است و توسعهدهندگان را قادر میسازد تا بدون نگرانی در مورد مدیریت سرور، بر منطق برنامهها تمرکز کنند. این رویکرد به خصوص با پلتفرم هایی مانند AWS محبوبیت فوق العاده ای به دست آورده است.
با وجود مزایای آن، مبتدیان اغلب برای یافتن منابع واضح برای شروع ساختن API های بدون سرور با چالش هایی مواجه می شوند. هدف این مقاله با ارائه یک راهنمای گام به گام برای ایجاد نقاط پایانی API بدون سرور برای یک برنامه تجارت الکترونیک کوچک با استفاده از AWS است.
این به طور خاص یک راهنمای گام به گام برای ایجاد نقاط پایانی CRUD API بدون سرور برای مدیریت محصولات در یک برنامه تجارت الکترونیک کوچک با استفاده از AWS ارائه میکند.
در زیر فهرستی از فناوریهای مورد نیاز برای ساخت این و نحوه کمک آنها به کل آرایش آمده است.
- Node.js – محیط زمان اجرا را برای کد برنامه ما فراهم می کند. برای جاوا اسکریپت سمت سرور، سبک و سریع است. پشتیبانی عالی از برنامه نویسی ناهمزمان را ارائه می دهد، که برای رسیدگی موثر به درخواست های HTTP و پرس و جوهای DynamoDB بسیار مهم است. همچنین به رسیدگی به درخواست ها و پاسخ ها با استفاده از AWS Lambda کمک می کند.
- TypeScript– ابر مجموعه ای از جاوا اسکریپت، تایپ استاتیک و ویژگی های جاوا اسکریپت مدرن را به پایگاه کد اضافه می کند، بنابراین کیفیت کد و قابلیت نگهداری را بهبود می بخشد. همچنین خطاهای مربوط به نوع را در طول توسعه شناسایی می کند و باگ های زمان اجرا را کاهش می دهد. همچنین تکمیل خودکار و تشخیص خطا را بهتر ارائه می دهد (چیزهای شگفت انگیز!😃).
- AWS Lambda – به اجرای کد برنامه ما به صورت بدون سرور کمک می کند. این نیاز به مدیریت سرورها و همچنین مزیت اضافی را که با استفاده به طور خودکار مقیاس می شود را از بین می برد. همچنین مقرون به صرفه است زیرا شما اساساً فقط هزینه زمان اجرا را پرداخت می کنید. هر عملیات API (به عنوان مثال، ایجاد یا فهرست کردن محصولات) به عنوان یک تابع Lambda جداگانه اجرا می شود. همچنین دارای یکپارچگی روان با دروازه API AWS برای پاسخ به درخواست های HTTP
- دروازه API AWS – به عنوان نقطه ورود درخواست های HTTP به توابع Lambda ما عمل می کند. توابع Lambda را به عنوان RESTful API در معرض نمایش می گذارد. ویژگی هایی مانند اعتبار سنجی درخواست، محدود کردن نرخ، و مجوز را ارائه می دهد. درخواست های HTTP (مثلاً POST / محصولات) را به تابع Lambda مناسب هدایت می کند.
- AWS DynamoDB – داده ها را برای برنامه (به عنوان مثال، اطلاعات محصول) ذخیره می کند. پایگاه داده NoSQL برای کارایی و مقیاس پذیری بالا طراحی شده است. همچنین، این مدل «پرداخت به ازای هر استفاده» با رویکرد بدون سرور هماهنگ است. هر محصول دارای یک کلید پارتیشن (PK) برای جستجوی کارآمد و یک کلید مرتب سازی (SK) برای ساختار اضافی است.
- چارچوب بدون سرور – استقرار و مدیریت برنامه های بدون سرور را ساده می کند. عمدتاً پیکربندیهای خاص AWS را انتزاع میکند. همچنین یک فایل پیکربندی یکپارچه مبتنی بر YAML برای تعریف منابع، توابع و پلاگین ها فراهم می کند.
- AWS SDK – به برنامه اجازه می دهد تا به صورت برنامه ریزی شده با سرویس های AWS تعامل داشته باشد. این API برای DynamoDB، Lambda، S3 و سایر سرویسهای AWS فراهم میکند. احراز هویت و ارتباط با AWS را ساده می کند.
- پستچی – ابزاری برای آزمایش نقاط پایانی API. عملکرد API را در طول توسعه و پس از استقرار تأیید می کند. این با ارسال POST، GET یا سایر درخواست های HTTP به نقاط پایانی و بررسی پاسخ ها به دست می آید.
این مقاله انتظار دارد که شما یک حساب AWS با ترجیحاً یک کاربر IAM راهاندازی کرده باشید که کاربر اصلی نباشد (برای پیروی از رویکرد کمترین امتیاز برای مدیریت کاربر/منبع AWS). همچنین لازم است حساب خود را با استفاده از AWS-CLI پیکربندی کنید. این را نمی توان در اینجا پوشش داد، اما منابع شگفت انگیزی در اینترنت برای یادگیری این موضوع وجود دارد. امیدوارم بتوانم این موضوع را در مقاله بعدی نیز پوشش دهم.
بدون مقدمه، بیایید در 🚀 شیرجه بزنیم
1) پیش نیازها
قبل از شروع کدنویسی، بسیار مهم است که موارد زیر را داشته باشیم:
- ما آخرین نسخه نصب شده زمان اجرا Node.js را داریم، می توانید آن را در اینجا پیدا کنید
- ما همانطور که در بالا ذکر شد یک حساب AWS راه اندازی کرده ایم، می توانید از اینجا شروع کنید
- ما هم باید داشته باشیم بدون سرور با استفاده از دستور زیر به صورت جهانی نصب شده است (در برخی موارد باید sudo یا دسترسی مدیر را اجرا کنید (برای ویندوز) زیرا ممکن است به مجوزهای root نیاز داشته باشید
npm install -g serverless
- سپس باید آن را راه اندازی کنید AWS CLI ابتدا آن را در اینجا نصب کنید و آن را با جزئیات دسترسی AWS خود با استفاده از دستور زیر پیکربندی کنید:
aws configure
- سپس باید کلید دسترسی AWS، کلید مخفی، منطقه و فرمت خروجی پیش فرض را وارد کنیم.
2) راه اندازی پروژه
- با در نظر گرفتن این پیش نیازهای اساسی، پس از آن باید با استفاده از دستور زیر، الگوی پروژه بدون سرور جدید خود را ایجاد کنیم.
serverless
من شخصاً توصیه میکنم الگوهای Node.js، Express و DynamoDB را انتخاب کنید تا کد boilerplate مورد نیاز برای بوت استرپ برنامه را کاهش دهید، یک مثال در زیر نشان داده شده است.
نام پروژه ایجاد شده را گذاشتم mini-commerce-api (شما می توانید هر نام دلخواه را انتخاب کنید). پوشه ای به همین نام ایجاد می کند که می توانید به صورت زیر به آن بروید.
cd mini-ecommerce-api
سپس به نصب وابستگیها و وابستگیهای توسعهدهنده مورد نیاز علاوه بر مواردی که هنگام ایجاد پروژه نصب شدهاند، به شرح زیر ادامه میدهیم.
npm install ts-node @types/node aws-sdk
npm install --save-dev typescript @types/aws-lambda
این خلاصه ای از بسته های اصلی نصب شده است
- تایپ اسکریپت: پشتیبانی از TypeScript.
- ts-node: TypeScript را مستقیماً بدون کامپایل کردن در JS اجرا می کند.
- @types/node: تعاریف Node.js را تایپ کنید.
- aws-sdk: کتابخانه AWS برای تعامل با سرویس های AWS مانند DynamoDB.
- @types/aws-lambda: پشتیبانی از نوع را برای نوشتن توابع AWS Lambda ارائه می دهد
3) تایپ اسکریپ را پیکربندی کنید
npx tsc --init
این باعث ایجاد یک tsconfig.json که سپس به صورت زیر ویرایش می کنیم
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
4) ساختار پروژه
سپس اقدام به ایجاد فایل ها و پوشه ها در ساختار زیر می کنیم
mini-ecommerce-api/
├── src/
│ ├── handlers/
│ │ ├── createProduct.ts
│ │ ├── listProducts.ts
├── getProductId.ts
├── updateProduct.ts
├── deleteProduct.ts
│ ├── utils/
│ │ ├── dynamoClient.ts
├── .gitignore
├── serverless.yml
├── tsconfig.json
├── package.json
5) پیکربندی Serverless.yml
سپس فایل serverless.yml پیشفرض تولید شده در پروژه را با فایل زیر جایگزین میکنیم که تعریف مناسبی از آنچه برای این مثال میسازیم ارائه میکند. این مثال را می توان با نیاز(های) پروژه خاص شما تنظیم کرد
#app metadata
org: geocoderserverless
app: my-serverless-app
service: mini-ecommerce-api-serverless
frameworkVersion: "4"
provider:
name: aws
runtime: nodejs18.x
region: ca-central-1 ## Change ca-central to your region of choice
environment: ## This binds your env variable to the dynamoDB product table
PRODUCTS_TABLE:
Ref: ProductsTable
## Lambda permissions.
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: arn:aws:dynamodb:ca-central-1:${aws:accountId}:table/ProductsTable ## Change the ca-central to the region of choice
## handler build configuration
## This causes each lambda function to be built and packaged individually, this was done to save space
package:
individually: true
exclude:
- node_modules/**
- .serverless/**
- .git/**
- test/**
- README.md
- package-lock.json
## Handler functions
functions:
createProduct:
handler: src/handlers/createProduct.createProduct ## The handler to create products
events:
- http:
path: products
method: post
listProducts:
handler: src/handlers/listProducts.listProducts
events:
- http:
path: products
method: get
getProductById:
handler: src/handlers/getProductById.getProductById
events:
- http:
path: products/{id}
method: get
updateProduct:
handler: src/handlers/updateProduct.updateProduct
events:
- http:
path: products/{id}
method: put
deleteProduct:
handler: src/handlers/deleteProduct.deleteProduct
events:
- http:
path: products/{id}
method: delete
#DynamoDB table configuration
resources:
Resources:
ProductsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ProductsTable
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
6) کد
با مرتب شدن قسمت اصلی پیکربندی، میتوانیم به نوشتن کد ادامه دهیم.
الف) راه اندازی DynamoDBClient (src/utils/DynamoDBClient.ts)
اول از همه، ما به کدی برای برقراری فراخوانی پایگاه داده با DynamoDB نیاز داریم، بنابراین موارد زیر را برای ایجاد یک تابع ابزار dynamoDBClient می نویسیم که به ما کمک می کند منطق تعامل با DynamoDB را به صورت زیر انتزاع کنیم:
src/utils/DynamoDBClient.ts
import { DynamoDB } from "aws-sdk";
const dynamoDB = new DynamoDB.DocumentClient()
export default dynamoDB
ب) ایجاد یک رابط محصول (src/utils/DynamoDBClient.ts)
برای تعاریف مناسب نوع، میتوانیم یک رابط ایجاد کنیم که مشخص کند یک محصول به چه چیزی نیاز دارد، این به ارائه ایمنی نوع هنگام نوشتن توابع Lambda ما کمک میکند.
export interface Product {
name: string;
price: number;
description?: string
}
ج) با این تنظیمات، میتوانیم به ایجاد کنترلکنندههایی که توابع لامبدا را برای اجرای عملیات CRUD فراخوانی میکنند، ادامه دهیم. تعاریف گرداننده در زیر نشان داده شده است.
من ایجاد کنترل کننده محصول (src/handlers/createProduct.ts)
import { APIGatewayProxyHandler } from 'aws-lambda';
import dynamoDB from '../utils/DynamoDbClient';
import { Product } from '../interfaces/Product';
import { DynamoDB } from 'aws-sdk';
// Handler definition
export const createProduct: APIGatewayProxyHandler = async (event) => {
const body: Partial = JSON.parse(event.body || '{}');
const { name, price, description } = body;
if (!name || !price) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Name and price are required!' }),
};
}
// Definition of parameters including input
const params: DynamoDB.DocumentClient.PutItemInput = {
TableName: process.env.PRODUCTS_TABLE!,
Item: {
PK: `PRODUCT#${name}`,
SK: `PRODUCT`,
name,
price,
description,
} as Product,
};
try {
await dynamoDB.put(params).promise();
return {
statusCode: 201,
body: JSON.stringify({
message: 'Product created successfully',
data: params.Item,
}),
};
} catch (error) {
console.error('Error creating product:', error);
return {
statusCode: 500,
body: JSON.stringify({
error: 'Could not create product',
}),
};
}
};
ii فهرست کننده محصولات (src/handlers/listProducts.ts)
import { APIGatewayProxyHandler } from 'aws-lambda'
import dynamoDB from '../utils/DynamoDbClient'
import {DynamoDB} from 'aws-sdk'
import { Product } from '../interfaces/Product'
export const listProducts: APIGatewayProxyHandler = async(event)=> {
const params: DynamoDB.DocumentClient.ScanInput = {
TableName: process.env.PRODUCTS_TABLE!,
}
try {
const result: DynamoDB.DocumentClient.ScanOutput = await dynamoDB.scan(params).promise()
const products: Product[] = result.Items as Product[] || []
return {
statusCode: 200,
body: JSON.stringify({
success: "true",
data: products
})
}
} catch (error) {
console.error('Error Fetching products:',error)
return {
statusCode: 500,
body: JSON.stringify({
error: 'Could not fetch products'
})
}
}
}
III. واکشی محصولات با شناسه (PK) Handler (src/handlers/getProductById.ts)
- برای سهولت درک این مقاله، نام محصول را به عنوان کلید اصلی/پارتیشن (PK) انتخاب کردم و اجرای آن در زیر نشان داده شده است.
import { APIGatewayProxyHandler } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
import dynamoDB from "../utils/DynamoDbClient";
import { Product } from "../interfaces/Product";
export const getProductById: APIGatewayProxyHandler = async(event)=>{
const productId = event.pathParameters?.id
if(!productId) {
return {
statusCode: 400,
body: JSON.stringify({
error: "Product ID is required"
})
}
}
const params: DynamoDB.DocumentClient.GetItemInput = {
TableName: process.env.PRODUCTS_TABLE!,
Key: {
PK: `PRODUCT#${productId}`,
SK: "PRODUCT"
}
}
try {
const result: DynamoDB.DocumentClient.GetItemOutput = await dynamoDB.get(params).promise()
const product: Product | undefined = result.Item as Product
if(!product) {
return {
statusCode: 404,
body: JSON.stringify({
error: "Product not found!"
})
}
}
return {
statusCode: 200,
body: JSON.stringify({
success: true,
data: product
})
}
} catch (error) {
console.error('Error fetching product:', error)
return {
statusCode: 500,
body: JSON.stringify({error: `Could not fetch product with id ${productId}`})
}
}
}
IV به روز رسانی یک محصول توسط کنترل کننده شناسه (src/handlers/updateProduct.ts)
import { APIGatewayProxyHandler } from "aws-lambda";
import { Product } from "../interfaces/Product";
import { DynamoDB } from "aws-sdk";
import dynamoDB from "../utils/DynamoDbClient";
export const updateProduct: APIGatewayProxyHandler = async (event)=>{
const productId = event.pathParameters?.id
const body: Partial = JSON.parse(event.body || "{}")
const {name, price, description} = body
if(!productId || (!name && !price && !description)) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'PRODUCT ID and least one field should be updated'
})
}
}
// set up to store edited values
const updateExpressions: string[] = []
const expressionAttributeNames: Record = {}
const expressionAttributeValues: Record = {}
if(name) {
updateExpressions.push("#name = :name")
expressionAttributeNames["#name"] = "name"
expressionAttributeValues[":name"] = name
}
if(description) {
updateExpressions.push("#description = :description")
expressionAttributeNames["#description"] = "description"
expressionAttributeValues[":description"] = description
}
if(price) {
updateExpressions.push("#price = :price")
expressionAttributeNames["#price"] = "price"
expressionAttributeValues[":price"] = price
}
// set up params
const params: DynamoDB.DocumentClient.UpdateItemInput = {
TableName: process.env.PRODUCTS_TABLE!,
Key: { PK: `PRODUCT#${productId}`, SK: "PRODUCT" },
UpdateExpression: `SET ${updateExpressions.join(", ")}`,
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: expressionAttributeValues,
ReturnValues: "ALL_NEW"
}
try {
const result: DynamoDB.DocumentClient.UpdateItemOutput = await dynamoDB.update(params).promise()
const updatedProduct: Product = result.Attributes as Product
return {
statusCode: 200,
body: JSON.stringify({
success: true,
data: updatedProduct
})
}
} catch (error) {
console.error("Error updating product:", error);
return {
statusCode: 500,
body: JSON.stringify({ error: "Could not update product." }),
};
}
}
v حذف یک محصول توسط شناسه شناسه (src/handlers/updateProduct.ts)
import { APIGatewayProxyHandler } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
import dynamoDB from "../utils/DynamoDbClient";
export const deleteProduct: APIGatewayProxyHandler = async (event)=> {
const productId = event.pathParameters?.id
if(!productId) {
return {
statusCode: 400,
body: JSON.stringify({
success: false,
error: 'Product ID is required'
})
}
}
const params: DynamoDB.DocumentClient.DeleteItemInput = {
TableName: process.env.PRODUCTS_TABLE!,
Key: {
PK: `PRODUCT#${productId}`,
SK: "PRODUCT",
},
ConditionExpression: "attribute_exists(PK)"
}
try {
await dynamoDB.delete(params).promise()
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: "Product deleted successfully"
})
}
} catch (error: any) {
console.error('Error deleting product:', error)
if (error.code === "ConditionalCheckFailedException") {
return {
statusCode: 404,
body: JSON.stringify({
success: false,
error: "Product not found.",
}),
};
}
return {
statusCode: 500,
body: JSON.stringify({
success: false,
error: "Could not delete product.",
}),
};
}
}
7) استقرار کد
سپس می توانیم از طریق تعاریف serverless.yml با استفاده از دستور زیر، سرویس را با کد تکمیل شده در AWS مستقر کنیم:
serverless deploy
ممکن است نیاز به آماده سازی وجود داشته باشد سودو (برای کاربران لینوکس) یا دسترسی سرپرست (برای کاربران ویندوز) را به دلیل مشکلات مجوز اضافه کنید.
این فرآیند توابع/هندلرهای لامبدا مقیاسپذیر را برای رسیدگی به درخواستها و پاسخها فراهم میکند، یک جدول Product DynamoDB ایجاد میکند و یک معماری بهینهشده با استفاده از CloudFormation ایجاد میکند.
در صورت موفقیت آمیز بودن، باید خروجی زیر را دریافت کنیم، به طوری که api-id و منطقه، api-id شخصی و منطقه AWS شما در قالب مثالی که در زیر نشان داده شده است:
endpoints:
POST - https://.execute-api..amazonaws.com/dev/products
GET - https://.execute-api..amazonaws.com/dev/products
GET - https://.execute-api..amazonaws.com/dev/products/{id}
PUT - https://.execute-api..amazonaws.com/dev/products/{id}
DELETE - https://.execute-api..amazonaws.com/dev/products/{id}
سپس میتوانیم در مورد من، نقاط پایانی دریافتشده از طریق یک مشتری frontend را آزمایش کنیم پستچی، در زیر تصاویری از نتایج را مشاهده می کنید
برای ایجاد یک محصول
برای دریافت تمام محصولات
برای دریافت یک محصول واحد با شناسه
برای به روز رسانی یک محصول واحد با شناسه
سپس میتوانیم با بررسی مجدد همه محصولات، تأیید کنیم که آیا محصول واقعاً بهروزرسانی شده است، زیرا میبینیم که محصول بهروزرسانی شده است.
برای حذف یک محصول واحد با شناسه
سپس میتوانیم یک تماس دیگر برای دریافت این محصول برقرار کنیم و ببینیم آیا وجود دارد یا خیر، همانطور که در زیر میبینیم که تعداد اقلام یک کاهش یافته است که نشان میدهد محصول واقعاً حذف شده است.
با این حال، آزمون نهایی تأیید این است که توابع Lambda و جدول DynamoDB ما با موفقیت ایجاد و به درستی پیکربندی شده اند. همانطور که در زیر نشان داده شده است، هر دو با موفقیت راه اندازی شدند، بله! 😁🕺🏿
در پایان، ما با موفقیت توابع Lambda بدون سرور را برای انجام عملیات CRUD و تعامل با پایگاه داده DynamoDB با استفاده از Node.js و TypeScript ساختهایم. در حالی که این به عنوان یک راهنمای اساسی عمل می کند، فضای کافی برای گسترش و تقویت این مفاهیم وجود دارد. اما هدف این مقاله ارائه یک نقطه شروع محکم برای توسعه دهندگانی است که پتانسیل معماری بدون سرور و کاربردهای عملی آن را بررسی می کنند.
می توانید کد منبع را در Github پیدا کنید
من همچنین آماده سوالات و تعاملات بیشتر در مورد این و سایر موضوعات کدگذاری فنی هستم.
یاد بگیرید، کدنویسی مبارک 🚀
من را در لینکدین اینجا دنبال کنید