هوش مصنوعی با Node.js؛ آموزش ساخت اپلیکیشن AI با Express و API درواره
در این آموزش عملی، با Node.js و Express یک Backend هوش مصنوعی میسازیم، آن را به API درواره متصل میکنیم و قابلیتهای چت، Streaming، خروجی JSON، اعتبارسنجی و مدیریت خطا را پیادهسازی میکنیم.
Node.js یکی از محبوبترین محیطها برای ساخت Backend، REST API، اپلیکیشنهای بلادرنگ و سرویسهای تحت وب است. اگر با JavaScript یا TypeScript کار میکنید، میتوانید بدون تغییر زبان برنامهنویسی، قابلیتهای هوش مصنوعی را نیز به Backend خود اضافه کنید.
ترکیب Node.js با API مدلهای هوش مصنوعی برای ساخت این محصولات مناسب است:
- چتبات پشتیبانی
- دستیار هوشمند داخل وبسایت
- ابزار تولید محتوا
- خلاصهساز متن
- تحلیل بازخورد مشتری
- تولید توضیحات محصول
- استخراج داده از متن
- دستیار برنامهنویسی
- سرویس ترجمه
- سیستم پرسشوپاسخ
- ایجنت هوش مصنوعی
- قابلیت هوشمند در محصولات SaaS
در این مقاله یک پروژه عملی و قابل توسعه میسازیم که از Node.js، Express و API هوش مصنوعی درواره استفاده میکند.
قابلیتهای نهایی پروژه:
- REST API استاندارد
- اتصال به API سازگار با OpenAI درواره
- دریافت پاسخ متنی
- پشتیبانی از تاریخچه مکالمه
- Streaming پاسخ
- دریافت خروجی JSON
- اعتبارسنجی ورودی و خروجی
- مدیریت خطاهای رایج
- Timeout و Retry کنترلشده
- ساختار مناسب برای توسعه
- امکان اتصال به React، Vue، Flutter یا اپلیکیشن موبایل
Node.js چیست و چرا برای هوش مصنوعی مناسب است؟
Node.js یک محیط اجرای متنباز و چندسکویی برای اجرای JavaScript خارج از مرورگر است. این محیط از موتور V8 استفاده میکند و برای عملیات ورودی و خروجی غیرمسدودکننده مانند درخواست شبکه، دیتابیس و فایل مناسب است. معرفی رسمی Node.js
در یک اپلیکیشن مبتنی بر API هوش مصنوعی، Backend بیشتر زمان خود را صرف انتظار برای این عملیات میکند:
- دریافت درخواست کاربر
- فراخوانی API مدل
- دریافت Stream
- خواندن یا ذخیرهسازی تاریخچه
- ارتباط با Redis یا PostgreSQL
- ارسال نتیجه به Client
مدل غیرمسدودکننده Node.js برای چنین Workflowهایی مناسب است.
مزایای مهم Node.js:
- استفاده از JavaScript در Frontend و Backend
- اکوسیستم گسترده npm
- پشتیبانی مناسب از Promise و
async/await - پیادهسازی ساده REST API
- پشتیبانی از Stream
- مناسب برای WebSocket و SSE
- امکان استفاده از TypeScript
- استقرار ساده روی سرور و Container
- کتابخانههای متعدد برای دیتابیس، Queue و Cache
Express چیست؟
Express فریمورکی سبک برای ساخت وبسرور و API روی Node.js است. با Express میتوانید Route، Middleware، Error Handler و پاسخ HTTP را مدیریت کنید.
طبق راهنمای رسمی Express، پس از ساخت پروژه با npm init میتوان Express را با دستور زیر نصب کرد: راهنمای نصب Express
npm install express
در این مقاله از Express برای ایجاد Endpointهای زیر استفاده میکنیم:
GET /health
POST /api/chat
POST /api/chat/stream
POST /api/analyze-feedback
تفاوت استفاده از ChatGPT و API در Node.js
ChatGPT یک محصول آماده برای تعامل مستقیم کاربر است. API هوش مصنوعی برای اتصال نرمافزار به مدل طراحی شده است.
| ویژگی | ChatGPT | API در Node.js |
|---|---|---|
| استفاده مستقیم کاربر | بله | از طریق اپلیکیشن شما |
| رابط اختصاصی | محدود | کاملاً قابل طراحی |
| اتصال به دیتابیس | مستقیم نیست | بله |
| کنترل System Prompt | محدودتر | بله |
| مدیریت کاربران | خارج از اختیار برنامه | در Backend شما |
| ثبت مصرف | محدود | قابل پیادهسازی |
| انتخاب مدل | در محیط محصول | با Model ID |
| ساخت Workflow | محدود | کاملاً برنامهپذیر |
| خروجی JSON | برای استفاده دستی | قابل اعتبارسنجی |
| اتصال به CRM و ERP | محدود | بله |
اگر هدف شما استفاده شخصی است، یک ابزار گفتوگویی کافی است. اگر میخواهید هوش مصنوعی بخشی از سایت، اپلیکیشن یا محصول شما باشد، به API نیاز دارید.
معماری پروژه
جریان درخواست به این شکل خواهد بود:
مرورگر یا اپلیکیشن
↓
Express REST API
↓
اعتبارسنجی ورودی
↓
سرویس هوش مصنوعی
↓
API درواره
↓
مدل هوش مصنوعی
↓
پاسخ Express
↓
مرورگر یا اپلیکیشن
کلید API درواره فقط در Backend قرار میگیرد. مرورگر و اپلیکیشن موبایل نباید به کلید اصلی دسترسی داشته باشند.
پیشنیازها
برای ادامه آموزش به این موارد نیاز دارید:
- Node.js
- npm
- ویرایشگر کد مانند VS Code
- آشنایی مقدماتی با JavaScript
- حساب درواره
- API Key درواره
- Model ID یکی از مدلهای متنی
برای ایجاد حساب و دریافت کلید به درواره مراجعه کنید. فهرست مدلها، شناسه و قیمت بهروز آنها در صفحه مدلهای درواره قرار دارد.
در نمونهکدها از Placeholderهای زیر استفاده میکنیم:
YOUR_DARVAREH_API_KEY
YOUR_MODEL_ID
آنها را با کلید و Model ID واقعی جایگزین کنید.
بررسی نصب Node.js
نسخه Node.js:
node --version
نسخه npm:
npm --version
بهتر است از یکی از نسخههای LTS پشتیبانیشده Node.js استفاده کنید.
ساخت پروژه
پوشه پروژه را بسازید:
mkdir nodejs-ai-api
cd nodejs-ai-api
فایل package.json را ایجاد کنید:
npm init -y
وابستگیهای اصلی:
npm install express openai dotenv cors zod
وابستگی Development:
npm install --save-dev nodemon
کاربرد بستهها:
| Package | کاربرد |
|---|---|
express | ساخت REST API |
openai | استفاده از SDK سازگار با API |
dotenv | خواندن متغیرهای محیطی |
cors | تنظیم دسترسی Frontend |
zod | اعتبارسنجی ورودی و خروجی |
nodemon | اجرای مجدد خودکار در Development |
کتابخانه رسمی JavaScript و TypeScript امکان ساخت Client و ارسال درخواست به API را فراهم میکند. مخزن رسمی OpenAI JavaScript SDK
تنظیم ES Modules
فایل package.json را به این شکل ویرایش کنید:
{
"name": "nodejs-ai-api",
"version": "1.0.0",
"description": "AI API with Node.js, Express and Darvareh",
"type": "module",
"main": "src/app.js",
"scripts": {
"dev": "nodemon src/app.js",
"start": "node src/app.js",
"test": "node --test"
},
"engines": {
"node": ">=20"
},
"dependencies": {
"cors": "^2",
"dotenv": "^16",
"express": "^5",
"openai": "^5",
"zod": "^3"
},
"devDependencies": {
"nodemon": "^3"
}
}
شماره نسخههای دقیق ثبتشده توسط npm ممکن است متفاوت باشد. پس از نصب، فایل package-lock.json را در Repository نگه دارید.
ساختار پروژه
ساختار پیشنهادی:
nodejs-ai-api/
├── src/
│ ├── ai/
│ │ ├── client.js
│ │ └── chat.service.js
│ ├── config/
│ │ └── env.js
│ ├── middleware/
│ │ └── error-handler.js
│ ├── routes/
│ │ ├── chat.routes.js
│ │ └── analysis.routes.js
│ └── app.js
├── test/
│ └── chat.service.test.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── package-lock.json
این ساختار باعث میشود Route، تنظیمات، Client و منطق تعامل با مدل در فایلهای جداگانه قرار بگیرند.
ساخت فایل متغیرهای محیطی
فایل .env:
PORT=3000
NODE_ENV=development
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL=YOUR_MODEL_ID
DARVAREH_BASE_URL=https://api.darvareh.ir/v1
AI_TIMEOUT_MS=30000
AI_MAX_RETRIES=2
فایل .env.example:
PORT=3000
NODE_ENV=development
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL=YOUR_MODEL_ID
DARVAREH_BASE_URL=https://api.darvareh.ir/v1
AI_TIMEOUT_MS=30000
AI_MAX_RETRIES=2
فایل .gitignore:
node_modules/
.env
coverage/
npm-debug.log*
.DS_Store
کلید API را در GitHub، کد Frontend یا فایل قابل دانلود قرار ندهید.
اعتبارسنجی تنظیمات برنامه
فایل:
src/config/env.js
کد:
import "dotenv/config";
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
PORT: z.coerce
.number()
.int()
.positive()
.default(3000),
DARVAREH_API_KEY: z
.string()
.min(1),
DARVAREH_MODEL: z
.string()
.min(1),
DARVAREH_BASE_URL: z
.string()
.url()
.default("https://api.darvareh.ir/v1"),
AI_TIMEOUT_MS: z.coerce
.number()
.int()
.positive()
.default(30000),
AI_MAX_RETRIES: z.coerce
.number()
.int()
.min(0)
.max(5)
.default(2),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error(
"Invalid environment configuration:",
parsed.error.flatten().fieldErrors,
);
process.exit(1);
}
export const env = parsed.data;
با این کار، برنامه در زمان شروع متوجه تنظیمات ناقص یا نامعتبر میشود.
ساخت Client اتصال به درواره
فایل:
src/ai/client.js
کد:
import OpenAI from "openai";
import { env } from "../config/env.js";
export const aiClient = new OpenAI({
apiKey: env.DARVAREH_API_KEY,
baseURL: env.DARVAREH_BASE_URL,
timeout: env.AI_TIMEOUT_MS,
maxRetries: env.AI_MAX_RETRIES,
});
آدرس پایه درواره:
https://api.darvareh.ir/v1
مدل در درخواست مشخص خواهد شد:
model: env.DARVAREH_MODEL
ساخت سرویس Chat
فایل:
src/ai/chat.service.js
کد:
import { aiClient } from "./client.js";
import { env } from "../config/env.js";
const defaultSystemPrompt = `
تو یک دستیار فارسی دقیق و کاربردی هستی.
پاسخ را روشن و متناسب با پرسش کاربر بنویس.
اطلاعاتی را که در اختیار نداری حدس نزن.
اگر سؤال مبهم است، ابهام را اعلام کن.
`.trim();
export async function generateChatResponse({
messages,
systemPrompt = defaultSystemPrompt,
temperature = 0.3,
maxTokens = 1000,
}) {
const response =
await aiClient.chat.completions.create({
model: env.DARVAREH_MODEL,
messages: [
{
role: "system",
content: systemPrompt,
},
...messages,
],
temperature,
max_tokens: maxTokens,
});
const answer =
response.choices[0]?.message?.content;
if (!answer || !answer.trim()) {
const error = new Error(
"مدل پاسخ قابل استفادهای تولید نکرد.",
);
error.statusCode = 502;
throw error;
}
return {
answer: answer.trim(),
model: env.DARVAREH_MODEL,
usage: response.usage ?? null,
};
}
export async function createChatStream({
messages,
systemPrompt = defaultSystemPrompt,
temperature = 0.3,
maxTokens = 1000,
}) {
return aiClient.chat.completions.create({
model: env.DARVAREH_MODEL,
messages: [
{
role: "system",
content: systemPrompt,
},
...messages,
],
temperature,
max_tokens: maxTokens,
stream: true,
});
}
طراحی Schema پیامها
هر پیام از دو فیلد تشکیل میشود:
{
"role": "user",
"content": "Node.js چیست؟"
}
نقشهای قابل قبول برای Endpoint عمومی پروژه:
userassistant
پیام system را از Client نمیپذیریم. System Prompt در Backend مدیریت میشود.
ساخت Route چت معمولی
فایل:
src/routes/chat.routes.js
کد:
import { Router } from "express";
import { z } from "zod";
import {
createChatStream,
generateChatResponse,
} from "../ai/chat.service.js";
export const chatRouter = Router();
const messageSchema = z.object({
role: z.enum([
"user",
"assistant",
]),
content: z
.string()
.trim()
.min(1)
.max(10000),
});
const chatRequestSchema = z.object({
messages: z
.array(messageSchema)
.min(1)
.max(30),
temperature: z
.number()
.min(0)
.max(2)
.optional(),
maxTokens: z
.number()
.int()
.min(1)
.max(4000)
.optional(),
});
chatRouter.post(
"/",
async (request, response, next) => {
try {
const parsed =
chatRequestSchema.safeParse(
request.body,
);
if (!parsed.success) {
return response.status(422).json({
error: "validation_error",
message:
"ساختار درخواست معتبر نیست.",
details:
parsed.error.flatten().fieldErrors,
});
}
const result =
await generateChatResponse({
messages:
parsed.data.messages,
temperature:
parsed.data.temperature,
maxTokens:
parsed.data.maxTokens,
});
return response.json({
data: result,
});
} catch (error) {
next(error);
}
},
);
نمونه Request:
{
"messages": [
{
"role": "user",
"content": "سه کاربرد Node.js در پروژههای هوش مصنوعی را توضیح بده."
}
],
"temperature": 0.3,
"maxTokens": 800
}
نمونه Response:
{
"data": {
"answer": "Node.js برای ساخت Backend، پردازش درخواستهای همزمان و پیادهسازی Streaming کاربرد دارد.",
"model": "YOUR_MODEL_ID",
"usage": {
"prompt_tokens": 40,
"completion_tokens": 35,
"total_tokens": 75
}
}
}
وجود و شکل دقیق usage میتواند به مدل و Endpoint وابسته باشد.
مدیریت تاریخچه مکالمه
برای مکالمه چندمرحلهای، پیامهای قبلی را در آرایه messages قرار دهید:
{
"messages": [
{
"role": "user",
"content": "Express چیست؟"
},
{
"role": "assistant",
"content": "Express یک فریمورک وب برای Node.js است."
},
{
"role": "user",
"content": "چه زمانی از آن استفاده کنم؟"
}
]
}
در یک محصول واقعی بهتر است Client تمام تاریخچه را بدون کنترل ارسال نکند. Backend میتواند conversationId دریافت و تاریخچه را از دیتابیس بازیابی کند.
ساختار پیشنهادی:
Conversation
- id
- user_id
- title
- created_at
- updated_at
Message
- id
- conversation_id
- role
- content
- created_at
ساخت Streaming API
Streaming باعث میشود پاسخ مدل بهتدریج برای کاربر ارسال شود.
کد Route را در همان فایل chat.routes.js اضافه کنید:
chatRouter.post(
"/stream",
async (request, response, next) => {
let headersSent = false;
try {
const parsed =
chatRequestSchema.safeParse(
request.body,
);
if (!parsed.success) {
return response.status(422).json({
error: "validation_error",
message:
"ساختار درخواست معتبر نیست.",
details:
parsed.error.flatten().fieldErrors,
});
}
const stream =
await createChatStream({
messages:
parsed.data.messages,
temperature:
parsed.data.temperature,
maxTokens:
parsed.data.maxTokens,
});
response.status(200);
response.setHeader(
"Content-Type",
"text/plain; charset=utf-8",
);
response.setHeader(
"Cache-Control",
"no-cache, no-transform",
);
response.setHeader(
"Connection",
"keep-alive",
);
response.setHeader(
"X-Content-Type-Options",
"nosniff",
);
response.flushHeaders();
headersSent = true;
for await (const chunk of stream) {
const content =
chunk.choices[0]?.delta?.content;
if (content) {
response.write(content);
}
}
response.end();
} catch (error) {
if (headersSent) {
response.end();
return;
}
next(error);
}
},
);
Client میتواند متن را بهصورت Stream بخواند و در رابط چت نمایش دهد.
آزمایش Streaming با cURL
curl -N \
-X POST \
http://localhost:3000/api/chat/stream \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Node.js را در پنج بند معرفی کن."
}
]
}'
گزینه -N بافر داخلی cURL را غیرفعال میکند تا Chunkها سریعتر نمایش داده شوند.
دریافت Stream در JavaScript مرورگر
async function streamChat(messages) {
const response = await fetch(
"http://localhost:3000/api/chat/stream",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages,
}),
},
);
if (!response.ok || !response.body) {
throw new Error(
"Streaming request failed",
);
}
const reader =
response.body.getReader();
const decoder =
new TextDecoder("utf-8");
let fullText = "";
while (true) {
const { value, done } =
await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(
value,
{
stream: true,
},
);
fullText += chunk;
console.log(fullText);
}
return fullText;
}
در React میتوانید fullText را پس از هر Chunk در State قرار دهید.
خروجی JSON با Node.js
در بسیاری از پروژهها پاسخ متنی کافی نیست. ممکن است بخواهید بازخورد مشتری را به این ساختار تبدیل کنید:
{
"sentiment": "negative",
"category": "delivery",
"priority": "high",
"summary": "سفارش با تأخیر تحویل شده است.",
"requiresHuman": true
}
برای دریافت نتیجه قابل استفاده باید:
- ساختار را دقیق تعریف کنید.
- از مدل بخواهید فقط JSON برگرداند.
- متن را Parse کنید.
- خروجی را با Schema اعتبارسنجی کنید.
- خطای JSON را مدیریت کنید.
- برای مدلهای پشتیبانیشده از Structured Outputs استفاده کنید.
ساخت Route تحلیل بازخورد
فایل:
src/routes/analysis.routes.js
کد:
import { Router } from "express";
import { z } from "zod";
import { aiClient }
from "../ai/client.js";
import { env }
from "../config/env.js";
export const analysisRouter = Router();
const feedbackRequestSchema = z.object({
text: z
.string()
.trim()
.min(3)
.max(10000),
});
const feedbackResultSchema = z.object({
sentiment: z.enum([
"positive",
"neutral",
"negative",
]),
category: z.enum([
"product",
"delivery",
"support",
"billing",
"other",
]),
priority: z.enum([
"low",
"medium",
"high",
]),
summary: z
.string()
.min(1)
.max(500),
requiresHuman: z.boolean(),
});
function cleanJsonText(text) {
return text
.trim()
.replace(/^```json\s*/i, "")
.replace(/^```\s*/i, "")
.replace(/\s*```$/i, "")
.trim();
}
analysisRouter.post(
"/feedback",
async (request, response, next) => {
try {
const requestResult =
feedbackRequestSchema.safeParse(
request.body,
);
if (!requestResult.success) {
return response.status(422).json({
error: "validation_error",
message:
"متن بازخورد معتبر نیست.",
details:
requestResult
.error
.flatten()
.fieldErrors,
});
}
const completion =
await aiClient
.chat
.completions
.create({
model: env.DARVAREH_MODEL,
messages: [
{
role: "system",
content: `
تو یک سیستم تحلیل بازخورد مشتری هستی.
فقط JSON معتبر برگردان.
هیچ توضیح یا Markdown اضافه نکن.
ساختار دقیق:
{
"sentiment": "positive | neutral | negative",
"category": "product | delivery | support | billing | other",
"priority": "low | medium | high",
"summary": "خلاصه فارسی",
"requiresHuman": true
}
اگر دسته مشخص نیست، other را انتخاب کن.
اطلاعات ناموجود را حدس نزن.
`.trim(),
},
{
role: "user",
content:
requestResult.data.text,
},
],
temperature: 0,
max_tokens: 400,
});
const rawContent =
completion
.choices[0]
?.message
?.content;
if (!rawContent) {
const error = new Error(
"مدل خروجی تولید نکرد.",
);
error.statusCode = 502;
throw error;
}
let parsedJson;
try {
parsedJson = JSON.parse(
cleanJsonText(rawContent),
);
} catch {
const error = new Error(
"خروجی مدل JSON معتبر نیست.",
);
error.statusCode = 502;
throw error;
}
const result =
feedbackResultSchema.safeParse(
parsedJson,
);
if (!result.success) {
const error = new Error(
"خروجی مدل با Schema مورد انتظار سازگار نیست.",
);
error.statusCode = 502;
error.details =
result
.error
.flatten()
.fieldErrors;
throw error;
}
return response.json({
data: result.data,
});
} catch (error) {
next(error);
}
},
);
پشتیبانی از response_format، JSON Schema و حالت Strict به مدل انتخابی وابسته است. پیش از استفاده در Production، قابلیت مدل را در صفحه مدلهای درواره بررسی و روی داده واقعی آزمایش کنید.
نمونه تحلیل بازخورد
Request:
{
"text": "سفارشم سه روز دیر رسید و پاسخ پشتیبانی هم خیلی دیر ارسال شد."
}
Response احتمالی:
{
"data": {
"sentiment": "negative",
"category": "delivery",
"priority": "high",
"summary": "مشتری از تأخیر سفارش و پاسخگویی کند پشتیبانی ناراضی است.",
"requiresHuman": true
}
}
اعتبارسنجی Schema فقط شکل داده را بررسی میکند. صحت معنایی خروجی همچنان باید ارزیابی شود.
ساخت Error Handler
فایل:
src/middleware/error-handler.js
کد:
import OpenAI from "openai";
export function errorHandler(
error,
request,
response,
next,
) {
if (response.headersSent) {
return next(error);
}
if (
error instanceof
OpenAI.AuthenticationError
) {
return response.status(500).json({
error: "ai_authentication_error",
message:
"تنظیمات سرویس هوش مصنوعی نامعتبر است.",
});
}
if (
error instanceof
OpenAI.RateLimitError
) {
return response.status(429).json({
error: "rate_limit_error",
message:
"تعداد درخواستها بیش از حد مجاز است.",
});
}
if (
error instanceof
OpenAI.APIConnectionError
) {
return response.status(503).json({
error: "ai_connection_error",
message:
"ارتباط با سرویس هوش مصنوعی برقرار نشد.",
});
}
if (
error instanceof
OpenAI.APIStatusError
) {
console.error({
name: error.name,
status: error.status,
requestId: error.request_id,
});
return response.status(502).json({
error: "upstream_ai_error",
message:
"سرویس هوش مصنوعی پاسخ موفقی نداد.",
});
}
const statusCode =
Number.isInteger(error.statusCode)
? error.statusCode
: 500;
if (statusCode >= 500) {
console.error(error);
}
return response.status(statusCode).json({
error: "request_failed",
message:
statusCode >= 500
? "پردازش درخواست با خطا مواجه شد."
: error.message,
details: error.details,
});
}
پارامتر next باید در امضای Middleware خطا وجود داشته باشد تا Express آن را Error Handler تشخیص دهد.
ساخت فایل اصلی Express
فایل:
src/app.js
کد:
import cors from "cors";
import express from "express";
import { env }
from "./config/env.js";
import { analysisRouter }
from "./routes/analysis.routes.js";
import { chatRouter }
from "./routes/chat.routes.js";
import { errorHandler }
from "./middleware/error-handler.js";
const app = express();
app.disable("x-powered-by");
app.use(
cors({
origin:
env.NODE_ENV === "production"
? [
"https://your-app.example",
]
: [
"http://localhost:5173",
"http://localhost:3001",
],
methods: [
"GET",
"POST",
],
allowedHeaders: [
"Content-Type",
"Authorization",
],
}),
);
app.use(
express.json({
limit: "100kb",
}),
);
app.get(
"/health",
(request, response) => {
response.json({
status: "ok",
service: "nodejs-ai-api",
environment: env.NODE_ENV,
});
},
);
app.use(
"/api/chat",
chatRouter,
);
app.use(
"/api/analyze",
analysisRouter,
);
app.use(
(request, response) => {
response.status(404).json({
error: "not_found",
message:
"مسیر درخواستشده وجود ندارد.",
});
},
);
app.use(errorHandler);
app.listen(
env.PORT,
() => {
console.log(
`Server running on http://localhost:${env.PORT}`,
);
},
);
در Production مقدار واقعی Origin فرانتاند خود را جایگزین کنید:
https://your-app.example
اجرای پروژه
Development:
npm run dev
Production:
npm start
بررسی سلامت:
curl http://localhost:3000/health
خروجی:
{
"status": "ok",
"service": "nodejs-ai-api",
"environment": "development"
}
آزمایش Chat Endpoint
curl \
-X POST \
http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Express را در سه بند معرفی کن."
}
]
}'
اتصال React به Backend
نمونه تابع ساده:
export async function sendChatMessage(
messages,
) {
const response = await fetch(
"http://localhost:3000/api/chat",
{
method: "POST",
headers: {
"Content-Type":
"application/json",
},
body: JSON.stringify({
messages,
}),
},
);
const body = await response.json();
if (!response.ok) {
throw new Error(
body.message
?? "Request failed",
);
}
return body.data;
}
در Production آدرس API را از متغیر محیطی Frontend دریافت کنید، اما کلید اصلی درواره را در Frontend قرار ندهید.
چرا ورودی را محدود کردیم؟
در Schema چت این محدودیتها وجود دارند:
messages: z
.array(messageSchema)
.min(1)
.max(30)
و:
content: z
.string()
.trim()
.min(1)
.max(10000)
دلایل:
- جلوگیری از درخواست خالی
- کنترل اندازه Context
- کاهش مصرف ناخواسته
- جلوگیری از فشار بیشازحد روی Backend
- پاسخگویی قابل پیشبینیتر
- مدیریت بهتر هزینه
در محصول واقعی محدودیت باید براساس کاربرد، مدل و بودجه تنظیم شود.
مدیریت توکن و هزینه
مدلهای متنی ورودی و خروجی را به واحدهایی به نام توکن (Token) پردازش میکنند.
مصرف معمولاً تحت تأثیر این موارد است:
- طول System Prompt
- تعداد پیامهای تاریخچه
- طول هر پیام
- Context اضافهشده
- طول پاسخ
- مدل انتخابی
- Retry
- فراخوانی ابزار
برای کاهش مصرف:
- پیامهای غیرضروری را حذف کنید.
- تاریخچه قدیمی را خلاصه کنید.
maxTokensرا براساس کاربرد محدود کنید.- پاسخ تکراری را Cache کنید.
- مدل مناسب وظیفه انتخاب کنید.
- برای دستهبندی از Prompt کوتاه استفاده کنید.
- مصرف را به تفکیک کاربر و Feature ثبت کنید.
- Retry را محدود کنید.
- درخواست تکراری Frontend را کنترل کنید.
برای قیمت بهروز مدلها به صفحه مدلهای درواره مراجعه کنید.
ثبت Usage
در پاسخ غیر Streaming میتوانید Usage را ذخیره کنید:
const usage = response.usage;
console.log({
promptTokens:
usage?.prompt_tokens,
completionTokens:
usage?.completion_tokens,
totalTokens:
usage?.total_tokens,
});
برای هر درخواست این اطلاعات مفید هستند:
- userId
- feature
- model
- promptTokens
- completionTokens
- totalTokens
- latencyMs
- status
- promptVersion
- createdAt
API Key و داده حساس کاربران را در Log ثبت نکنید.
Timeout و Retry
در Client تنظیم کردیم:
timeout: env.AI_TIMEOUT_MS,
maxRetries: env.AI_MAX_RETRIES,
Retry فقط برای خطاهای موقت مفید است. خطاهای زیر معمولاً با تکرار حل نمیشوند:
- API Key اشتباه
- Model ID نامعتبر
- ورودی نامعتبر
- اعتبار ناکافی
- پارامتر پشتیبانینشده
Retry در چند لایه میتواند باعث تکرار بیشازحد درخواست شود. اگر SDK، Service و Queue هر سه Retry داشته باشند، تعداد فراخوانیها بهسرعت افزایش مییابد.
لغو درخواست با AbortController
برای درخواستهایی که باید قابل لغو باشند، میتوانید از AbortController استفاده کنید:
const controller =
new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
30000,
);
try {
const response =
await aiClient
.chat
.completions
.create(
{
model:
env.DARVAREH_MODEL,
messages: [
{
role: "user",
content:
"Node.js چیست؟",
},
],
},
{
signal:
controller.signal,
},
);
console.log(response);
} finally {
clearTimeout(timeoutId);
}
پشتیبانی جزئیات Optionها را با نسخه SDK نصبشده بررسی کنید.
ذخیره تاریخچه در PostgreSQL
برای محصول واقعی میتوانید از PostgreSQL استفاده کنید.
جداول ساده:
CREATE TABLE conversations (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
title VARCHAR(200),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE messages (
id UUID PRIMARY KEY,
conversation_id UUID NOT NULL
REFERENCES conversations(id)
ON DELETE CASCADE,
role VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_messages_conversation_created
ON messages(conversation_id, created_at);
جریان پیشنهادی:
- Client یک
conversationIdمیفرستد. - Backend مالکیت گفتگو را بررسی میکند.
- پیام جدید ذخیره میشود.
- پیامهای مرتبط اخیر خوانده میشوند.
- درخواست مدل اجرا میشود.
- پاسخ دستیار ذخیره میشود.
- نتیجه برای Client ارسال میشود.
Cache پاسخها با Redis
Cache برای تمام مکالمات مناسب نیست؛ اما در درخواستهای تکراری میتواند مفید باشد:
- توضیح ثابت محصول
- تولید FAQ از داده ثابت
- خلاصه سند بدون تغییر
- دستهبندی محتوای تکراری
- ترجمه متن یکسان
کلید Cache باید این موارد را در نظر بگیرد:
feature
model
promptVersion
normalizedInput
relevantSettings
تغییر Prompt یا مدل باید Cache Key را تغییر دهد.
مدیریت کارهای طولانی
برای وظایفی که ممکن است زمان زیادی ببرند، Request همزمان مناسب نیست.
نمونهها:
- تحلیل صدها سند
- تولید گزارش طولانی
- پردازش Batch
- ساخت Embedding برای داده زیاد
- تحلیل تعداد زیادی بازخورد
- اجرای Workflow چندمرحلهای
معماری پیشنهادی:
Client
↓
POST /jobs
↓
ساخت Job در دیتابیس
↓
افزودن به Queue
↓
Worker
↓
API درواره
↓
ذخیره نتیجه
↓
Webhook یا Polling
ابزارهایی مانند Redis و Queueهای Node.js میتوانند برای اجرای Worker استفاده شوند.
استفاده از TypeScript
برای پروژههای تیمی، TypeScript میتواند خطاهای نوع را زودتر مشخص کند.
نصب:
npm install --save-dev \
typescript \
@types/node \
@types/express \
@types/cors
نمونه Type:
type ChatRole =
| "user"
| "assistant";
interface ChatMessage {
role: ChatRole;
content: string;
}
interface ChatRequest {
messages: ChatMessage[];
temperature?: number;
maxTokens?: number;
}
بااینحال، TypeScript جایگزین اعتبارسنجی Runtime نیست. داده ورودی HTTP همچنان باید با Zod یا ابزار مشابه بررسی شود.
تست سرویس بدون فراخوانی مدل واقعی
در Unit Test نباید برای هر اجرا درخواست واقعی و پرهزینه ارسال شود. Client را Inject یا Mock کنید.
نمونه سرویس قابل تست:
export function createChatService({
client,
model,
}) {
return {
async generate(messages) {
const response =
await client
.chat
.completions
.create({
model,
messages,
});
const content =
response
.choices[0]
?.message
?.content;
if (!content) {
throw new Error(
"Empty model response",
);
}
return content;
},
};
}
Fake Client:
const fakeClient = {
chat: {
completions: {
create: async () => ({
choices: [
{
message: {
content:
"پاسخ آزمایشی",
},
},
],
}),
},
},
};
تست با Node Test Runner:
import assert
from "node:assert/strict";
import test
from "node:test";
import {
createChatService,
} from "../src/ai/testable-chat.service.js";
test(
"returns model content",
async () => {
const fakeClient = {
chat: {
completions: {
create: async () => ({
choices: [
{
message: {
content:
"پاسخ آزمایشی",
},
},
],
}),
},
},
};
const service =
createChatService({
client: fakeClient,
model: "YOUR_MODEL_ID",
});
const result =
await service.generate([
{
role: "user",
content: "سلام",
},
]);
assert.equal(
result,
"پاسخ آزمایشی",
);
},
);
سناریوهای مهم تست
Chat Endpoint
- پیام معتبر
- آرایه پیام خالی
- نقش نامعتبر
- متن خالی
- تعداد پیام بیش از سقف
- پاسخ خالی مدل
- خطای Rate Limit
- خطای اتصال
- Timeout
- Model ID نامعتبر
Structured Output
- JSON معتبر
- JSON داخل Markdown
- JSON ناقص
- فیلد حذفشده
- مقدار خارج از Enum
- نوع داده اشتباه
- پاسخ خالی
- متن غیر JSON
Streaming
- دریافت چند Chunk
- پایان موفق
- قطع اتصال Client
- خطا قبل از ارسال Header
- خطا پس از شروع Stream
- پاسخ بدون Content
- لغو درخواست
نکات مهم برای Production
کلید API را فقط در Backend نگه دارید
اشتباه:
const apiKey =
"کلید واقعی در فایل Frontend";
روش مناسب:
const apiKey =
process.env.DARVAREH_API_KEY;
System Prompt را سمت سرور مدیریت کنید
اگر Client اجازه تغییر System Prompt داشته باشد، رفتار سرویس از کنترل Backend خارج میشود.
برای هر کاربر محدودیت مصرف تعریف کنید
محدودیتهای پیشنهادی:
- درخواست در دقیقه
- توکن در روز
- بودجه ماهانه
- حداکثر طول ورودی
- حداکثر تعداد پیام
- تعداد Stream همزمان
درخواستهای تکراری را کنترل کنید
هنگام Loading دکمه ارسال را غیرفعال کنید. برای عملیات مهم میتوانید شناسه Idempotency در نظر بگیرید.
دادهها را قبل از عملیات اعتبارسنجی کنید
خروجی مدل نباید بدون بررسی:
- وارد دیتابیس شود.
- Query اجرا کند.
- ایمیل ارسال کند.
- وضعیت سفارش را تغییر دهد.
- تابع خارجی را فراخوانی کند.
مدل را از کد جدا کنید
مدل را در متغیر محیطی قرار دهید:
DARVAREH_MODEL=YOUR_MODEL_ID
در این صورت تغییر مدل به بازنویسی منطق اصلی نیاز ندارد.
کاربردهای واقعی Node.js و هوش مصنوعی
چتبات پشتیبانی
Backend پیام کاربر را دریافت و براساس Context محصول پاسخ پیشنهادی ایجاد میکند.
تحلیل تیکت
خروجی:
{
"category": "billing",
"priority": "high",
"summary": "کاربر درباره مبلغ صورتحساب اعتراض دارد."
}
تولید توضیحات محصول
ورودی:
- نام محصول
- ویژگیها
- مخاطب
- لحن
خروجی:
- عنوان
- توضیح کوتاه
- توضیح کامل
- مزایا
- پرسشهای متداول
خلاصهسازی جلسه
Backend متن جلسه را دریافت و این موارد را استخراج میکند:
- خلاصه
- تصمیمها
- مسئول هر اقدام
- موعدها
- سؤالات حلنشده
دستیار برنامهنویسی
قابلیتها:
- توضیح کد
- تولید تست
- Code Review
- ساخت مستندات
- پیشنهاد ریفکتور
- تحلیل Stack Trace
تحلیل بازخورد
هزاران نظر مشتری را میتوان به دسته، احساس، اولویت و خلاصه تبدیل کرد.
انتخاب مدل مناسب
| کاربرد | معیار اصلی |
|---|---|
| چت فارسی | کیفیت فارسی، سرعت و هزینه |
| دستهبندی | ثبات، سرعت و قیمت |
| کدنویسی | توانایی Coding |
| خروجی JSON | پشتیبانی ساختاریافته |
| خلاصهسازی | Context Window |
| Reasoning | دقت در مسائل چندمرحلهای |
| Tool Calling | پشتیبانی از ابزار |
| اپ پرترافیک | Latency، قیمت و پایداری |
برای مقایسه مدلها و قیمت، صفحه مدلهای درواره را بررسی کنید.
اشتباهات رایج
قراردادن API Key در React
هر متغیری که وارد Bundle مرورگر شود قابل مشاهده است. کلید اصلی را فقط در Backend قرار دهید.
ارسال تمام تاریخچه
این کار هزینه و زمان پاسخ را افزایش میدهد. تاریخچه را محدود یا خلاصه کنید.
نداشتن Validation
ورودی HTTP و خروجی مدل هر دو باید بررسی شوند.
نداشتن Timeout
Request بدون Timeout ممکن است منابع سرور را بیشازحد درگیر کند.
Retry نامحدود
Retry نامحدود میتواند هزینه و ترافیک را افزایش دهد.
اعتماد به JSON مدل
JSON را Parse و با Schema اعتبارسنجی کنید.
استفاده از یک مدل برای تمام وظایف
مدل مناسب دستهبندی ممکن است با مدل مناسب برنامهنویسی یا تحلیل طولانی متفاوت باشد.
نداشتن نسخه Prompt
برای Prompt شناسه تعریف کنید:
const PROMPT_VERSION =
"feedback-analysis-v1";
نمایش خطای داخلی به کاربر
جزئیات فنی را در Log کنترلشده ثبت کنید و پیام عمومی مناسب به کاربر برگردانید.
پرسشهای متداول
آیا میتوان با Node.js اپلیکیشن هوش مصنوعی ساخت؟
بله. Node.js میتواند Backend اپلیکیشن را بسازد، درخواستها را به API مدل ارسال کند و قابلیتهایی مانند چت، تولید متن، تحلیل، RAG و Agent را مدیریت کند.
چگونه Node.js را به API درواره متصل کنیم؟
یک Client با کلید و Base URL بسازید:
const client = new OpenAI({
apiKey:
"YOUR_DARVAREH_API_KEY",
baseURL:
"https://api.darvareh.ir/v1",
});
سپس Model ID را در درخواست قرار دهید:
model: "YOUR_MODEL_ID"
Base URL درواره چیست؟
https://api.darvareh.ir/v1
Model ID را از کجا پیدا کنیم؟
شناسه مدلها در صفحه مدلهای درواره قرار دارد.
آیا میتوان API Key را در JavaScript مرورگر قرار داد؟
خیر. JavaScript مرورگر برای نگهداری Secret مناسب نیست. درخواست باید از Backend Node.js ارسال شود.
Express برای ساخت API هوش مصنوعی مناسب است؟
بله. Express برای ساخت REST API، Middleware، Streaming و اتصال Frontend به سرویس هوش مصنوعی مناسب است.
آیا بهتر است از JavaScript یا TypeScript استفاده کنیم؟
برای نمونه ساده JavaScript سریعتر است. برای پروژه تیمی و بزرگ، TypeScript میتواند خوانایی و کنترل نوع بهتری ایجاد کند. هر دو به Validation Runtime نیاز دارند.
Streaming چیست؟
Streaming یعنی پاسخ مدل بهجای انتظار برای تکمیل کامل، بهصورت تدریجی برای Client ارسال شود.
آیا Streaming هزینه را کم میکند؟
معمولاً Streaming فقط نحوه دریافت پاسخ را تغییر میدهد و لزوماً مصرف توکن را کاهش نمیدهد.
چگونه خروجی JSON معتبر بگیریم؟
ساختار دقیق تعریف کنید، از قابلیت Structured Output مدل سازگار استفاده کنید و خروجی را در Backend با Schema اعتبارسنجی کنید.
آیا میتوان تاریخچه چت را در PostgreSQL ذخیره کرد؟
بله. گفتگو و پیامها را در جداول جدا ذخیره و برای هر درخواست فقط Context مرتبط را بازیابی کنید.
چگونه هزینه را کنترل کنیم؟
مدل مناسب انتخاب کنید، طول ورودی و خروجی را محدود کنید، تاریخچه را مدیریت کنید، Cache داشته باشید و مصرف را به تفکیک کاربر ثبت کنید.
آیا میتوان React یا Flutter را به این Backend متصل کرد؟
بله. هر Client که بتواند درخواست HTTP ارسال کند میتواند از Endpointهای Express استفاده کند.
آیا برای هر درخواست باید System Prompt بفرستیم؟
System Prompt معمولاً در Backend تعریف و همراه درخواست مدل ارسال میشود. برای کنترل رفتار سرویس بهتر است Client امکان تغییر مستقیم آن را نداشته باشد.
جمعبندی
Node.js و Express مسیر مناسبی برای ساخت Backend اپلیکیشنهای هوش مصنوعی فراهم میکنند. اگر از قبل با JavaScript کار میکنید، میتوانید بدون یادگیری یک زبان Backend جدید، مدلهای هوش مصنوعی را به وبسایت، اپلیکیشن یا سرویس سازمانی متصل کنید.
در این مقاله یاد گرفتیم چگونه:
- پروژه Node.js و Express بسازیم.
- تنظیمات را با متغیر محیطی مدیریت کنیم.
- به API سازگار با OpenAI درواره متصل شویم.
- REST API چت بسازیم.
- تاریخچه مکالمه را ارسال کنیم.
- پاسخ را بهصورت Streaming دریافت کنیم.
- بازخورد مشتری را به JSON تبدیل کنیم.
- خروجی را با Zod اعتبارسنجی کنیم.
- خطا، Timeout و Retry را مدیریت کنیم.
- Usage و مصرف توکن را ثبت کنیم.
- پروژه را برای PostgreSQL، Redis و Queue توسعه دهیم.
- Frontendهای React و Flutter را به Backend متصل کنیم.
برای اجرای پروژه در درواره ثبتنام کنید، کلید API بسازید و یک مدل متناسب با کیفیت، سرعت و بودجه پروژه از صفحه مدلها و قیمتها انتخاب کنید.
مقالات مرتبط
- API هوش مصنوعی چیست؟ راهنمای دریافت و استفاده
- دریافت API هوش مصنوعی و ساخت API Key
- API سازگار با OpenAI چیست؟
- اتصال API هوش مصنوعی به اپلیکیشن
- ساخت چتبات هوش مصنوعی با Next.js و React
- Streaming API چیست؟ آموزش دریافت تدریجی پاسخ
- Structured Outputs چیست؟ آموزش دریافت خروجی JSON
- Token چیست و هزینه API چگونه محاسبه میشود؟
- مدیریت خطا و Fallback در API هوش مصنوعی
- ساخت Dockerfile و Docker Compose با هوش مصنوعی
- ساخت CI/CD با GitHub Actions و هوش مصنوعی
- چگونه هزینه API هوش مصنوعی را کاهش دهیم؟
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.