كل واجهة API عامة تحتاج في النهاية إلى البنية ذاتها: طريقة لإصدار المفاتيح للعملاء، وطريقة للتحقق منها في كل طلب، وحدود لمعدّل الطلبات لكل مفتاح لإيقاف الإساءة، وحصص استخدام مرتبطة بخطط الفوترة، وصلاحيات دقيقة. بناء كل ذلك بنفسك يعني جدول مفاتيح، ومنطق تجزئة (hashing)، وعنقود Redis للعدّادات، وخط أنابيب للتحليلات، ولوحة تحكّم. إنها أسابيع من العمل الذي لا يميّز منتجك.
Unkey منصّة مفتوحة المصدر لإدارة مفاتيح API وتحديد معدّل الطلبات، تختزل كل ذلك في بضعة استدعاءات لحزمة تطوير. أنت تُنشئ مفتاحاً، وتتحقق من مفتاح، وتتولّى Unkey التجزئة، والتحقق العالمي منخفض الكمون، وتحديد المعدّل، والأرصدة القائمة على الاستخدام، والأدوار، والصلاحيات، والتحليلات. والمشروع قابل للاستضافة الذاتية بالكامل، وهذا أمرٌ مهم للفِرَق في أسواق منطقة الشرق الأوسط وشمال إفريقيا المنظَّمة التي تعمل تحت قواعد إقامة البيانات الخاصة بـ INPDP وقوانين حماية البيانات الشخصية (PDPL).
في هذا الدرس ستبني واجهة API لمعالجة المستندات لتطبيق SaaS افتراضي يصادق فيه كل عميل بمفتاح API. ستُصدر المفاتيح برمجياً، وتحمي المسارات بمساعد Next.js، وتفرض حدود المعدّل وحصص الأرصدة لكل مفتاح، وتُقفل نقاط النهاية المميّزة خلف الصلاحيات.
المتطلبات المسبقة
قبل البدء، تأكد من توفّر ما يلي:
- Node.js 20+ مثبّتاً
- حساب Unkey مجاني — سجّل على app.unkey.com
- معرفة أساسية بـ React و TypeScript
- إلمام بـ App Router في Next.js (Route Handlers، الـ middleware)
- محرّر شيفرة (يُوصى بـ VS Code)
ما الذي ستبنيه
واجهة API للمستندات لتطبيق SaaS تتضمّن:
- إصدار المفاتيح برمجياً — توليد مفتاح API محدود النطاق عند تسجيل عميل جديد
- التحقق من الطلبات — التحقق من كل مفتاح وارد باستدعاء واحد
- تحديد المعدّل لكل مفتاح — التحكّم بكل عميل على حدة
- أرصدة الاستخدام — قياس استدعاءات API مقابل حصة الخطة مع إعادة شحن تلقائية
- صلاحيات قائمة على الأدوار — قصر نقاط النهاية المميّزة على المفاتيح التي تحمل الصلاحية الصحيحة
- تحديد معدّل مستقل — حماية المسارات غير المصادَق عليها حسب عنوان IP
كيف يعمل Unkey
ثلاثة مفاهيم تحمل المنظومة بأكملها:
- المفتاح الجذري (root key) — مفتاح ذو امتيازات يستخدمه الخادم الخلفي لديك لاستدعاء واجهة الإدارة في Unkey (الإنشاء، التحقق، الإلغاء). لا يغادر خادمك أبداً ويعيش في متغيّر بيئة.
- API — مساحة أسماء في Unkey تجمع كل المفاتيح التي تُصدرها. لكل API معرّف
apiId. - مفاتيح العملاء — المفاتيح التي تسلّمها لمستخدميك. لا تخزّن Unkey سوى تجزئة؛ ويُعرَض النص الصريح مرة واحدة فقط عند الإنشاء.
القاعدة الذهبية: التحقق يُعيد دائماً HTTP 200. المفتاح المرفوض ليس خطأ نقل — عليك فحص الحقل valid (والرمز code) في جسم الاستجابة لتقرّر ما إذا كنت ستسمح بالطلب.
الخطوة 1: إنشاء مشروع Next.js
هيّئ مشروع Next.js جديداً مع TypeScript و App Router:
npx create-next-app@latest unkey-docs-api --typescript --app --src-dir --eslint
cd unkey-docs-apiثبّت حِزم Unkey:
npm install @unkey/api @unkey/nextjs @unkey/ratelimit@unkey/api— حزمة الإدارة لإنشاء المفاتيح والتحقق منها من خادمك الخلفي@unkey/nextjs— غلاف خفيف يتحقق من المفاتيح داخل الـ Route Handlers@unkey/ratelimit— تحديد معدّل مستقل للمسارات التي لا تحمل مفتاح API (تسجيل الدخول، التسجيل، نقاط النهاية العامة)
الخطوة 2: إعداد مساحة عمل Unkey
في لوحة تحكّم Unkey:
- أنشئ API جديدة (سمِّها
documents-api). انسخ معرّف API الخاص بها — يبدو مثلapi_3xZ.... - اذهب إلى Settings → Root Keys وأنشئ مفتاحاً جذرياً يحمل على الأقل هذه الصلاحيات:
api.*.create_keyوapi.*.verify_keyوratelimit.*.limit. انسخه — فهو يُعرَض مرة واحدة فقط.
أنشئ ملف .env.local:
UNKEY_ROOT_KEY=unkey_3y...
UNKEY_API_ID=api_3xZ...لا تُودِع هذا الملف في Git أبداً. المفتاح الجذري حسّاس بقدر حسّاسية كلمة مرور قاعدة البيانات — فمن يحمله يستطيع سكّ مفاتيح مقابل واجهتك.
الخطوة 3: إصدار مفتاح API عند تسجيل العميل
عندما ينضم عميل جديد، يطلب خادمك الخلفي من Unkey سكّ مفتاح محدود بحسابه. أنشئ مساعداً للخادم فقط في src/lib/unkey.ts:
import { Unkey } from "@unkey/api";
// نسخة واحدة مشتركة، يُعاد استخدامها عبر التطبيق كله.
export const unkey = new Unkey({
rootKey: process.env.UNKEY_ROOT_KEY!,
});
interface IssueKeyInput {
userId: string;
plan: "free" | "pro" | "enterprise";
}
// ربط خطة الفوترة بحصة شهرية من الطلبات.
const PLAN_CREDITS = {
free: 1_000,
pro: 50_000,
enterprise: 1_000_000,
} as const;
export async function issueApiKey({ userId, plan }: IssueKeyInput) {
const result = await unkey.keys.createKey({
apiId: process.env.UNKEY_API_ID!,
prefix: "docs", // تبدو المفاتيح مثل docs_xxxxxxxx — سهلة التمييز
name: `مفتاح ${plan} للمستخدم ${userId}`,
externalId: userId, // يربط المفتاح بسجل المستخدم الخاص بك
meta: { plan },
// حد معدّل لكل خطة، يُطبَّق تلقائياً في كل عملية تحقق.
ratelimits: [
{
name: "requests",
limit: plan === "enterprise" ? 100 : plan === "pro" ? 50 : 10,
duration: 10_000, // لكل 10 ثوانٍ
autoApply: true,
},
],
// حصة استخدام شهرية يُعاد شحنها في الأول من كل شهر.
credits: {
remaining: PLAN_CREDITS[plan],
refill: {
interval: "monthly",
amount: PLAN_CREDITS[plan],
refillDay: 1,
},
},
// عملاء الباقة المميّزة يحصلون أيضاً على صلاحية "documents.export".
permissions:
plan === "enterprise"
? ["documents.read", "documents.write", "documents.export"]
: ["documents.read", "documents.write"],
});
// المفتاح الصريح يُعاد مرة واحدة فقط. أعِده إلى المستدعي الآن —
// لن تتمكن من قراءته مجدداً أبداً.
return result;
}تفاصيل تستحق الفهم:
externalIdيربط مفتاح Unkey بمعرّفuserIdالخاص بك. لاحقاً يمكنك سرد أو إلغاء كل مفتاح يخصّ مستخدماً دون تخزين المفتاح بنفسك.autoApply: trueعلى حدّ معدّل يعني أن Unkey تفرضه تلقائياً في كل استدعاءverifyKey— لست بحاجة لتمرير الحد مجدداً وقت التحقق.creditsتحوّل كل عملية تحقق إلى وحدة مَقيسة. عندما يبلغremainingصفراً يتوقف المفتاح عن التحقق حتى إعادة الشحن التالية.
الآن اكشف هذا عبر Route Handler. أنشئ src/app/api/keys/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { issueApiKey } from "@/lib/unkey";
export async function POST(req: NextRequest) {
// في تطبيق حقيقي، صادِق على جلسة لوحة التحكم أولاً هنا.
const { userId, plan } = await req.json();
if (!userId || !plan) {
return NextResponse.json(
{ error: "userId و plan مطلوبان" },
{ status: 400 },
);
}
const { result, error } = await issueApiKey({ userId, plan });
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
// result.key هو النص الصريح — اعرضه على العميل مرة واحدة فقط.
return NextResponse.json({ key: result.key, keyId: result.keyId });
}تُعيد الحزمة بنية { result, error }، لذا لست بحاجة أبداً لتغليف الاستدعاءات بـ try/catch للأخطاء المتوقّعة من API — بل تتفرّع بناءً على error.
الخطوة 4: حماية مسار باستخدام withUnkey
أبسط طريقة لحماية Route Handler هي الغلاف withUnkey من @unkey/nextjs. يقرأ المفتاح من ترويسة Authorization: Bearer، ويتحقق منه، ويُرفق النتيجة بـ req.unkey.
أنشئ src/app/api/documents/route.ts:
import { withUnkey } from "@unkey/nextjs";
import { NextResponse } from "next/server";
export const POST = withUnkey(
async (req) => {
// ينفّذ withUnkey معالِجك فقط حين يكون المفتاح حاضراً بنيوياً.
// ومع ذلك عليك التحقق من صلاحيته بنفسك:
if (!req.unkey?.valid) {
return NextResponse.json(
{ error: "غير مصرّح", code: req.unkey?.code },
{ status: 401 },
);
}
// req.unkey يحمل كل ما هيّأته: ownerId و meta و permissions.
const plan = (req.unkey.meta?.plan as string) ?? "free";
return NextResponse.json({
message: "تم قبول المستند للمعالجة",
plan,
remaining: req.unkey.remaining, // الأرصدة المتبقية بعد هذا الاستدعاء
});
},
{
rootKey: process.env.UNKEY_ROOT_KEY!,
},
);اختبره. سُكّ مفتاحاً أولاً:
curl -X POST http://localhost:3000/api/keys \
-H "Content-Type: application/json" \
-d '{"userId":"user_42","plan":"pro"}'
# { "key": "docs_3a8...", "keyId": "key_..." }ثم استدعِ المسار المحمي بهذا المفتاح:
curl -X POST http://localhost:3000/api/documents \
-H "Authorization: Bearer docs_3a8..." \
-H "Content-Type: application/json" \
-d '{"title":"تقرير الربع الثالث"}'
# { "message": "تم قبول المستند للمعالجة", "plan": "pro", "remaining": 49999 }استدعِه بدون مفتاح — أو بمفتاح عشوائي — وستحصل على 401. لاحظ أن remaining نقص بمقدار واحد: حصة الأرصدة تُفرَض تلقائياً.
الخطوة 5: التحقق من المفاتيح يدوياً للتحكّم الكامل
withUnkey مريح، لكن واجهات API الحقيقية كثيراً ما تحتاج للتحقق داخل منطقها الخاص — مثلاً لفحص صلاحية محدّدة، أو فرض تكلفة رصيد متغيّرة، أو تطبيق حدّ معدّل مُسمّى. استخدم unkey.keys.verifyKey مباشرة.
أنشئ حارساً قابلاً لإعادة الاستخدام في src/lib/auth.ts:
import { unkey } from "./unkey";
interface VerifyOptions {
permission?: string; // مثل "documents.export"
cost?: number; // كم رصيداً يستهلكه هذا الطلب
}
export async function verifyRequest(
authHeader: string | null,
opts: VerifyOptions = {},
) {
const key = authHeader?.replace(/^Bearer\s+/i, "");
if (!key) {
return { ok: false as const, status: 401, reason: "missing_key" };
}
const { result, error } = await unkey.keys.verifyKey({
key,
// استعلام صلاحية: يجب أن يحمل المفتاح هذه الصلاحية لكي يمرّ.
permissions: opts.permission,
// فرض أكثر من رصيد واحد للعمليات الثقيلة.
credits: opts.cost ? { cost: opts.cost } : undefined,
});
if (error) {
// خطأ نقل/إدارة (وليس مفتاحاً مرفوضاً) — أغلِق افتراضياً.
return { ok: false as const, status: 500, reason: error.message };
}
if (!result.valid) {
// result.code يخبرك بالسبب: NOT_FOUND، RATE_LIMITED،
// USAGE_EXCEEDED، INSUFFICIENT_PERMISSIONS، EXPIRED، DISABLED...
const status = result.code === "RATE_LIMITED" ? 429 : 403;
return { ok: false as const, status, reason: result.code };
}
return { ok: true as const, key: result };
}الآن ابنِ نقطة نهاية مميّزة تتطلّب صلاحية documents.export وتكلّف 5 أرصدة لكل استدعاء. أنشئ src/app/api/documents/export/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { verifyRequest } from "@/lib/auth";
export async function POST(req: NextRequest) {
const auth = await verifyRequest(req.headers.get("authorization"), {
permission: "documents.export",
cost: 5, // التصدير مكلف — قِسْه بثقل أكبر
});
if (!auth.ok) {
return NextResponse.json(
{ error: auth.reason },
{ status: auth.status },
);
}
// مفاتيح enterprise وحدها تحمل documents.export، لذا تحصل مفاتيح free/pro على 403.
return NextResponse.json({
message: "بدأ التصدير",
remaining: auth.key.remaining,
});
}مفتاح pro يستدعي هذه النقطة يتلقّى 403 INSUFFICIENT_PERMISSIONS، بينما ينجح مفتاح enterprise ويستهلك 5 أرصدة. الاستدعاء نفسه فرض المصادقة والترخيص والحصة في رحلة ذهاب وإياب واحدة.
الخطوة 6: تحديد معدّل المسارات غير المصادَق عليها حسب IP
بعض المسارات لا تحمل مفتاح API بعد — نموذج تسجيل عام، نقطة نهاية اتصال، صندوق بحث في التوثيق. استخدم حزمة @unkey/ratelimit المستقلة مفهرسةً على عنوان IP للعميل.
أنشئ src/lib/ratelimit.ts:
import { Ratelimit } from "@unkey/ratelimit";
export const publicLimiter = new Ratelimit({
rootKey: process.env.UNKEY_ROOT_KEY!,
namespace: "public-signup", // عزل هذا الحد عن مساحات الأسماء الأخرى
limit: 5, // 5 محاولات...
duration: "60s", // ...في الدقيقة لكل معرّف
async: true, // أطلِق وانسَ لأقل كمون ممكن
});طبّقه في Route Handler عام في src/app/api/signup/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { publicLimiter } from "@/lib/ratelimit";
export async function POST(req: NextRequest) {
// خلف وكيل (proxy)، فضّل المُدخل الأيسر في x-forwarded-for.
const ip =
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
const { success, remaining, reset } = await publicLimiter.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "محاولات تسجيل كثيرة جداً. أعِد المحاولة قريباً." },
{
status: 429,
headers: {
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
},
);
}
// ...تابِع إنشاء الحساب
return NextResponse.json({ ok: true });
}الرّاية async: true تجعل المُحدِّد يردّ تفاؤلياً بينما يزامن العدّادات في الخلفية. يقلّص الكمون مقابل السماح بمرور دفعة ضئيلة عند الحواف — مقايضة جيدة لنماذج التسجيل، وخاطئة لنقطة نهاية للدفع حيث ينبغي أن تضبط async: false.
الخطوة 7: إلغاء المفاتيح وسردها
عندما يخفّض عميل خطته أو يتركها أو يسرّب مفتاحاً، فإنك تُلغيه. ولأنك ضبطت externalId على userId الخاص بك، يمكنك أيضاً تعداد كل مفتاح يملكه المستخدم.
import { unkey } from "@/lib/unkey";
// إبطال مفتاح واحد فوراً عبر keyId الخاص به.
export async function revokeKey(keyId: string) {
return unkey.keys.deleteKey({ keyId });
}
// تعطيل مؤقت دون حذف (مثلاً عند فشل الدفع).
export async function suspendKey(keyId: string) {
return unkey.keys.updateKey({ keyId, enabled: false });
}المفتاح المُلغى أو المُعطَّل يفشل في أول عملية تحقق تالية برمز NOT_FOUND أو DISABLED — لا يوجد تأخير انتشار تديره من جانبك.
اختبار تنفيذك
اعبُر دورة الحياة الكاملة من الطرفية:
# 1. إصدار مفتاح بخطة free
curl -s -X POST http://localhost:3000/api/keys \
-H "Content-Type: application/json" \
-d '{"userId":"user_99","plan":"free"}'
# 2. اطرُق نقطة نهاية المستندات بما يتجاوز حدّ 10 لكل 10 ثوانٍ
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST http://localhost:3000/api/documents \
-H "Authorization: Bearer docs_YOURKEY" \
-H "Content-Type: application/json" -d '{}'
done
# يجب أن ترى تحوّل رموز 200 إلى 429 بمجرد امتلاء النافذة.
# 3. جرّب نقطة نهاية التصدير بمفتاح free -> 403
curl -s -X POST http://localhost:3000/api/documents/export \
-H "Authorization: Bearer docs_YOURKEY"للتغطية الآلية، تحقق من الفروع الحاسمة الثلاثة: مفتاح صالح يُعيد 200، ومفتاح مُحدَّد المعدّل يُعيد 429 برمز code: "RATE_LIMITED"، ومفتاح free يطرق /export يُعيد 403 برمز code: "INSUFFICIENT_PERMISSIONS".
استكشاف الأخطاء وإصلاحها
كل عملية تحقق تُعيد valid: false برمز NOT_FOUND. الأرجح أن مفتاحك الجذري يفتقر إلى صلاحية verify_key، أو أنك تتحقق مقابل API خاطئة. أكّد نطاقات المفتاح الجذري في لوحة التحكم، وأن المفتاح أُنشئ تحت apiId نفسه.
حدود المعدّل لا تُفعَّل أبداً. تحقق من أن حدّ المعدّل على المفتاح يحمل autoApply: true، أو أنك تمرّر مصفوفة ratelimits وقت التحقق. الحد المُعرَّف على المفتاح دون تطبيق تلقائي لا يُفرَض إلا حين تشير إليه بالاسم أثناء التحقق.
الأرصدة لا تنقص أبداً. يستهلك verifyKey الأرصدة فقط إذا أُنشئ المفتاح بكائن credits. المفاتيح المسكوكة بدونه غير محدودة بحكم التصميم.
قفزات كمون على المسارات المحمية. التحقق استدعاء شبكي. أبقِ عميل Unkey نسخةً وحيدة على مستوى الوحدة (كما في الخطوة 3) لإعادة استخدام الاتصالات، وفضّل النشر قرب حافة Unkey. للحدود غير الحرجة، يزيل async: true رحلة الذهاب والإياب من المسار الساخن.
قائمة التحقق للإنتاج
- لا تكشف المفتاح الجذري للمتصفح أبداً. كل استدعاءات إدارة Unkey تنتمي إلى Route Handlers أو server actions، لا إلى مكوّنات العميل.
- أغلِق افتراضياً. إذا أعاد
verifyKeyخطأ نقلerror، فارفض الطلب بدلاً من السماح بمروره. - اعرض المفتاح الصريح مرة واحدة. قدّمه في لوحة التحكم عند الإنشاء، ثم خزّن
keyIdفقط من جانبك. - اختَر
asyncبقصد. استخدمasync: falseحيث تتفوّق الدقة على الكمون (الفوترة، الكتابات الحسّاسة للإساءة). - استضِف ذاتياً لأجل إقامة البيانات. يمكن للفِرَق تحت INPDP أو PDPL تشغيل Unkey على بنيتها التحتية الخاصة بحيث لا تغادر مادة المفاتيح وتحليلات الاستخدام المنطقة أبداً.
الخطوات التالية
- أضِف أدواراً إلى جانب الصلاحيات لإدارة الوصول على شكل حزم بدلاً من صلاحية واحدة في كل مرة.
- أظهِر تحليلات Unkey في لوحة تحكّم عميلك لعرض اتجاهات الاستخدام لكل مفتاح.
- ادمج هذا مع نشر باستضافة ذاتية عبر سير عمل النشر باستضافة ذاتية مع Coolify v4.
- اقرِن مفاتيح API بالتخزين المؤقت على الحافة — راجع درس تحديد المعدّل والتخزين المؤقت مع Upstash Redis لنهج مكمّل.
الخاتمة
بنيت طبقة مصادقة API كاملة دون كتابة سطر واحد من شيفرة تجزئة المفاتيح أو إدارة العدّادات. منحتك Unkey إصدار المفاتيح برمجياً، والتحقق باستدعاء واحد، وحدود المعدّل لكل مفتاح، وأرصدة الاستخدام المرتبطة بخطط الفوترة، والإقفال بالصلاحيات — كل ذلك من ثلاث حِزم تطوير. تتوسّع البنية من مشروع جانبي مجاني إلى تطبيق SaaS مؤسسي، ولأن Unkey مفتوحة المصدر وقابلة للاستضافة الذاتية، فإنها تتكامل بنظافة مع متطلبات إقامة البيانات التي تعمل تحتها فِرَق منطقة الشرق الأوسط وشمال إفريقيا. في المرة القادمة التي تطلق فيها واجهة API عامة، اختر طبقة مفاتيح مُدارة بدلاً من إعادة بنائها من الصفر.