انتقل إلى المحتوى
التوثيق بالعربية

هذه ترجمة للصفحة الأصلية بالإنجليزية. عند وجود أي اختلاف، تُعتمد النسخة الإنجليزية.

درس تعليمي: ابنِ واجهة برمجة لمكتبة كتب

سنبني من البداية إلى النهاية خدمة صغيرة لمكتبة كتب: المسارات، والتحقق، والأمان، وOpenAPI، وSDK مكتوب الأنواع مع Hey API، واختبارات العقود. في النهاية سيكون لديك تطبيق DaloyJS بهيئة جاهزة للإنتاج.

ما الذي ستبنيه
  1. 01إنشاء المشروعpnpm add @daloyjs/core zod
  2. 02مصنع buildAppالمسارات · Zod · خطّافات الأمان
  3. 03التشغيلserve(app) على Node
  4. 04مواصفة OpenAPIgenerateOpenAPI(app)
  5. 05SDK مكتوب الأنواعHey API openapi-ts
  6. 06اختبارات العقودrunContractTests(app)
يتشارك الخادم وتصدير OpenAPI والاختبارات مصنع buildApp واحدًا، لذا لا يمكن أن تتباعد المواصفة والعميل مكتوب الأنواع واختبارات العقود أبدًا.

1. أنشئ المشروع

bash
mkdir bookstore && cd bookstore
pnpm init
pnpm add @daloyjs/core zod
pnpm add -D typescript @types/node @hey-api/openapi-ts prettier
json
// package.json، استبدله بما يلي
{
  "name": "bookstore",
  "type": "module",
  "scripts": {
    "dev":         "node --watch src/server.ts",
    "test":        "node --test tests/**/*.test.ts",
    "typecheck":   "tsc --noEmit",
    "gen:openapi": "node scripts/dump-openapi.ts",
    "gen:client":  "openapi-ts",
    "gen":         "pnpm gen:openapi && pnpm gen:client"
  }
}
json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "types": ["node"],
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "openapi-ts.config.ts"]
}
ini
# .npmrc
auto-install-peers=true
strict-peer-dependencies=true
prefer-frozen-lockfile=true
verify-store-integrity=true

2. ابنِ مصنع buildApp مشتركًا

شارك الـ App بين الخادم وتوليد الشيفرة والاختبارات لتبقى المواصفة والعميل والاختبارات متوافقة:

ts
// src/build-app.ts
import { z } from "zod";
import { App, requestId, secureHeaders, cors, rateLimit, bearerAuth, NotFoundError, timingSafeEqual } from "@daloyjs/core";

export const BookSchema = z.object({
  id:    z.string(),
  title: z.string(),
  year:  z.number().int().optional(),
});

export function buildApp() {
  const books = new Map<string, z.infer<typeof BookSchema>>([
    ["1", { id: "1", title: "Foundation", year: 1951 }],
    ["2", { id: "2", title: "Dune",       year: 1965 }],
  ]);

  const app = new App({ bodyLimitBytes: 64 * 1024, requestTimeoutMs: 5_000 });

  app.use(requestId());
  app.use(secureHeaders());
  app.use(cors({ origin: ["http://localhost:5173"] }));
  app.use(rateLimit({ windowMs: 60_000, max: 120 })); // عام ما لم تضبط keyGenerator أو trustProxyHeaders

  app.get(
    "/books/:id",
    {
      operationId: "getBookById",
      tags: ["Books"],
      request: { params: z.object({ id: z.string() }) },
      responses: {
        200: {
          description: "Found",
          body: BookSchema,
          examples: { default: { id: "1", title: "Foundation", year: 1951 } },
        },
        404: { description: "Not found" },
      },
    },
    async ({ params }) => {
      const book = books.get(params.id);
      if (!book) throw new NotFoundError(`book ${params.id} not found`);
      return { status: 200, body: book };
    },
  );

  app.post(
    "/books",
    {
      operationId: "createBook",
      tags: ["Books"],
      hooks: bearerAuth({ validate: (t) => timingSafeEqual(t, "demo-token") }),
      request: { body: BookSchema.omit({ id: true }) },
      responses: {
        201: { description: "Created", body: BookSchema },
        401: { description: "Unauthorized" },
        422: { description: "Validation error" },
      },
    },
    async ({ body }) => {
      const id = String(books.size + 1);
      const book = { id, ...body };
      books.set(id, book);
      return { status: 201, body: book };
    },
  );

  return app;
}

3. شغّل الخادم

ts
// src/server.ts
import { buildApp } from "./build-app.js";
import { serve }    from "@daloyjs/core/node";

const app = buildApp();
const { port } = serve(app, { port: 3000 });
console.log(`bookstore listening on http://localhost:${port}`);
bash
pnpm dev
curl http://localhost:3000/books/1
# {"id":"1","title":"Foundation","year":1951}

curl -X POST http://localhost:3000/books \
  -H "authorization: Bearer demo-token" \
  -H "content-type: application/json" \
  -d '{"title":"Hyperion","year":1989}'
# {"id":"3","title":"Hyperion","year":1989}

4. ولّد مواصفة OpenAPI

ts
// scripts/dump-openapi.ts
import { mkdir, writeFile } from "node:fs/promises";
import { dirname }          from "node:path";
import { generateOpenAPI }  from "@daloyjs/core/openapi";
import { buildApp }         from "../src/build-app.js";

const app  = buildApp();
const out  = "./generated/openapi.json";
const doc  = generateOpenAPI(app, {
  info: { title: "Bookstore API", version: "1.0.0" },
  securitySchemes: { bearer: { type: "http", scheme: "bearer" } },
});

await mkdir(dirname(out), { recursive: true });
await writeFile(out, JSON.stringify(doc, null, 2));
console.log(`wrote ${out}`);
bash
pnpm gen:openapi

5. ولّد SDK مكتوب الأنواع مع Hey API

ts
// openapi-ts.config.ts
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
  input: "./generated/openapi.json",
  output: { path: "./generated/client", postProcess: ["prettier"] },
  plugins: ["@hey-api/client-fetch", "@hey-api/typescript", "@hey-api/sdk"],
});
bash
pnpm gen
# generated/client/{client.gen.ts, sdk.gen.ts, types.gen.ts, index.ts}

6. استخدم الـ SDK من أي مستهلك TS

ts
import { client } from "../generated/client/client.gen.js";
import { getBookById } from "../generated/client/sdk.gen.js";

client.setConfig({ baseUrl: "http://localhost:3000" });

const { data } = await getBookById({ path: { id: "1" } });
console.log(data?.title); // string | undefined، مكتوب الأنواع بالكامل

7. أضف الاختبارات

ts
// tests/books.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../src/build-app.js";
import { runContractTests } from "@daloyjs/core/contract";

test("contract is clean", async () => {
  const report = await runContractTests(buildApp());
  assert.equal(report.ok, true, JSON.stringify(report.issues, null, 2));
});

test("GET /books/:id returns 200", async () => {
  const res = await buildApp().request("/books/1");
  assert.equal(res.status, 200);
});

test("POST /books rejects without token", async () => {
  const res = await buildApp().request("/books", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ title: "Hyperion" }),
  });
  assert.equal(res.status, 401);
});
bash
pnpm test

ما الذي بنيته

  • واجهة HTTP مكتوبة الأنواع ومُتحقَّق منها ومحمية.
  • مستند OpenAPI 3.1 حقيقي وSDK مكتوب الأنواع مولَّد، يبقيان متزامنين.
  • اختبارات عقود تحرس من التباين في التكامل المستمر.
  • مسار تثبيت معزَّز باستخدام pnpm وملف .npmrc محكم.

تابع مع الأمان، ثم درس التفويض متعدد المستخدمين، وAdapters، ومرجع الواجهة البرمجية، وهذه الثلاثة بالإنجليزية.

آخر تحديث: