الكتابات/tutorial/2026/06
Tutorial29 يونيو 2026·28 دقيقة

Better-T-Stack: إنشاء تطبيق TypeScript متكامل وآمن الأنواع من الطرف إلى الطرف في 2026

تعلّم كيفية إنشاء مستودع أحادي (monorepo) متكامل وآمن الأنواع من الطرف إلى الطرف باستخدام Better-T-Stack. يغطي هذا الدليل العملي أداة CLI وHono وtRPC وDrizzle وBetter Auth وإطلاق تطبيق full-stack يعمل فعليًا.

عادةً ما يعني إطلاق مشروع TypeScript متكامل حديث ربط نصف دزينة من الأدوات يدويًا: إطار عمل للواجهة الأمامية، وخادم خلفي، وطبقة API، وأداة ORM، ونظام مصادقة، وأداة فحص للكود (linting)، ونظام بناء للمستودع الأحادي. لكل منها إعداداته الخاصة، والفواصل بينها هي بالضبط حيث يتسرّب أمان الأنواع.

يزيل Better-T-Stack هذا الاحتكاك. إنه أداة CLI مفتوحة المصدر (create-better-t-stack) تُنشئ مستودعًا أحاديًا آمن الأنواع من الطرف إلى الطرف، حيث يتشارك مخطط قاعدة بياناتك وعقود الـAPI وكود الواجهة الأمامية مجموعة واحدة من الأنواع. غيّر عمودًا في المخطط فيُنبّهك TypeScript إلى المكوّن المعطوب في الواجهة قبل أن تشغّل التطبيق إطلاقًا.

في هذا الدرس ستُنشئ تطبيقًا كاملًا — خادم Hono وAPI من tRPC وطبقة بيانات Drizzle وBetter Auth وواجهة TanStack Router — ثم تربط ميزة صغيرة من الطرف إلى الطرف لإثبات أن أمان الأنواع حقيقي.

المتطلبات المسبقة

قبل البدء، تأكّد من توفّر ما يلي:

  • Bun 1.1+ مثبّت (curl -fsSL https://bun.sh/install | bash) — يستخدم Better-T-Stack محرك Bun كبيئة تشغيل ومدير حزم افتراضي. كما يعمل Node.js 20+ أيضًا.
  • معرفة أساسية بـTypeScript — ستظهر الأنواع العامة (generics) والاستنتاج (inference).
  • إلمام بـReact — الواجهة الأمامية الافتراضية هي React مع TanStack Router.
  • محرّر أكواد مزوّد بخادم لغة TypeScript (يُوصى بـVS Code).
  • نحو 30 دقيقة.

ما الذي ستبنيه

في النهاية سيكون لديك مستودع أحادي يعمل ويتضمّن تطبيقين:

  • apps/web — واجهة أمامية بـReact (TanStack Router + Tailwind + shadcn/ui)
  • apps/server — خادم Hono يكشف عن API من tRPC مدعوم بـDrizzle وSQLite

ستضيف جدول notes، وتكشف عن إجراءَي create وlist بأنواع محدّدة، وتستهلكهما من مكوّن React — كل ذلك دون كتابة أي نقطة نهاية REST واحدة أو استدعاء fetch مكتوب الأنواع يدويًا.

الخطوة 1: إنشاء المشروع

يأتي Better-T-Stack بأمر واحد. يمكنك تشغيله بشكل تفاعلي والإجابة عن الأسئلة، أو تمرير الرايات (flags) لإعداد قابل لإعادة الإنتاج. لنبدأ بالوضع التفاعلي لترى كل خيار:

bun create better-t-stack@latest

ترشدك الأداة عبر اختيار الواجهة الأمامية والخلفية وطبقة الـAPI وقاعدة البيانات وأداة ORM والمصادقة والإضافات. لهذا الدرس اختر:

  • الواجهة الأمامية: React → TanStack Router
  • الخلفية: Hono
  • الـAPI: tRPC
  • بيئة التشغيل: Bun
  • قاعدة البيانات: SQLite
  • ORM: Drizzle
  • المصادقة: Better Auth
  • الإضافات: Turborepo وBiome
  • المثال: Todo (يمنحك ميزة مرجعية للتعلّم منها)

تفضّل أمرًا واحدًا قابلًا لإعادة الإنتاج؟ مرّر الخيارات كرايات بدلًا من الإجابة عن الأسئلة:

bun create better-t-stack@latest my-app \
  --frontend tanstack-router \
  --backend hono \
  --api trpc \
  --runtime bun \
  --database sqlite \
  --orm drizzle \
  --auth \
  --addons turborepo biome \
  --examples todo \
  --install

تثبّت الراية --install التبعيات تلقائيًا. عند الانتهاء، يصبح لديك مستودع أحادي كامل. لا يوجد هنا أي ارتباط بمزوّد محدّد — كل تبعية هي حزمة مفتوحة المصدر قياسية كان بإمكانك تثبيتها بنفسك؛ الأداة تزيل فقط عناء الربط.

يمكنك أيضًا استخدام أداة Stack Builder المرئية على better-t-stack.dev/new للنقر على خياراتك ونسخ الأمر المُولَّد. إنها أسرع طريقة لاستكشاف مصفوفة الخيارات الكاملة دون حفظ أسماء الرايات.

الخطوة 2: فهم البنية المُولَّدة

انتقل إلى المشروع وألقِ نظرة على التخطيط:

cd my-app

يبدو المستودع الأحادي المُولَّد النموذجي هكذا:

my-app/
├── apps/
│   ├── web/                 # واجهة React الأمامية
│   │   ├── src/
│   │   │   ├── routes/      # مسارات TanStack Router
│   │   │   ├── components/
│   │   │   └── utils/trpc.ts  # عميل tRPC مكتوب الأنواع
│   │   └── package.json
│   └── server/              # خلفية Hono
│       ├── src/
│       │   ├── routers/     # موجِّهات tRPC
│       │   ├── db/          # مخطط Drizzle + العميل
│       │   ├── lib/auth.ts  # إعداد Better Auth
│       │   └── index.ts     # نقطة دخول Hono
│       └── package.json
├── turbo.json               # خط أنابيب Turborepo
├── biome.json               # إعداد المُنسّق/الفاحص
└── package.json             # جذر مساحة العمل

الفكرة الأساسية: يُصدّر apps/server نوع موجِّه tRPC الخاص به، ويستورد apps/web ذلك النوع لبناء عميل مكتوب الأنواع بالكامل. لا شيء يعبر الشبكة دون نوع.

الخطوة 3: تشغيل التطبيق لأول مرة

هيّئ قاعدة البيانات وشغّل كلا التطبيقين. يربط Better-T-Stack أداة Turborepo بحيث يشغّل أمر واحد المستودع الأحادي بأكمله:

# دفع مخطط Drizzle إلى قاعدة بيانات SQLite المحلية
bun run db:push
 
# تشغيل الواجهة والخادم معًا
bun dev

افتح الرابط المطبوع (عادةً http://localhost:3001 لتطبيق الويب). إن اخترت مثال Todo، فسترى قائمة تعمل مدعومة بالـAPI. يعمل الخادم على منفذه الخاص (غالبًا 3000) ويوجّه تطبيق الويب استدعاءات tRPC إليه.

إن أبلغ bun dev عن غياب ملف بيئة، انسخ ملف .env.example المُولَّد إلى .env داخل apps/server أولًا. يحتاج Better Auth إلى BETTER_AUTH_SECRET — أنشئه عبر openssl rand -base64 32.

الخطوة 4: إضافة جدول قاعدة بيانات بـDrizzle

الآن لنضف ميزتنا الخاصة. افتح مخطط Drizzle في apps/server/src/db/schema وعرّف جدول notes:

// apps/server/src/db/schema/notes.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
 
export const notes = sqliteTable("notes", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  body: text("body").notNull().default(""),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .$defaultFn(() => new Date()),
});
 
// أنواع مستنتجة يمكنك إعادة استخدامها في كل مكان
export type Note = typeof notes.$inferSelect;
export type NewNote = typeof notes.$inferInsert;

تأكّد من إعادة تصدير المخطط من ملف التجميع (غالبًا apps/server/src/db/schema/index.ts)، ثم ادفع التغيير إلى SQLite:

bun run db:push

يقرأ Drizzle تعريف TypeScript الخاص بك ويزامن الجدول — دون ملف ترحيل SQL منفصل تكتبه يدويًا أثناء التطوير المحلي. النوعان Note وNewNote أصبحا الآن المصدر الوحيد للحقيقة لهذا الكيان.

الخطوة 5: كشف إجراء tRPC مكتوب الأنواع

إجراء tRPC هو ببساطة دالة بمدخل مُتحقَّق منه ومخرج مكتوب الأنواع. أنشئ موجِّهًا للملاحظات:

// apps/server/src/routers/notes.ts
import { z } from "zod";
import { desc } from "drizzle-orm";
import { router, publicProcedure } from "../lib/trpc";
import { db } from "../db";
import { notes } from "../db/schema/notes";
 
export const notesRouter = router({
  list: publicProcedure.query(async () => {
    return db.select().from(notes).orderBy(desc(notes.createdAt));
  }),
 
  create: publicProcedure
    .input(
      z.object({
        title: z.string().min(1, "العنوان مطلوب").max(120),
        body: z.string().max(2000).optional(),
      }),
    )
    .mutation(async ({ input }) => {
      const [created] = await db
        .insert(notes)
        .values({ title: input.title, body: input.body ?? "" })
        .returning();
      return created;
    }),
});

يتحقّق مخطط z.object(...) من المدخل في وقت التشغيل و يستنتج نوع المدخل في وقت الترجمة. إن أرسل المستدعي شكلًا خاطئًا، يرفضه tRPC قبل تشغيل المعالج الخاص بك.

الآن سجّل الموجِّه في موجِّه التطبيق الجذري:

// apps/server/src/routers/index.ts
import { router } from "../lib/trpc";
import { notesRouter } from "./notes";
 
export const appRouter = router({
  notes: notesRouter,
  // ...موجِّهات أخرى (todo، إلخ.)
});
 
// هذا النوع المُصدَّر هو ما تستهلكه الواجهة الأمامية
export type AppRouter = typeof appRouter;

سطر export type AppRouter هذا هو الحيلة بأكملها. يُمحى في وقت البناء — فلا يَشحن أي كود وقت تشغيل — ومع ذلك يحمل الشكل الكامل لكل إجراء عبر حدود الحزمة.

الخطوة 6: استهلاك الـAPI من React

في الواجهة الأمامية، يستورد عميل tRPC المُولَّد بالفعل AppRouter. تستدعي الإجراءات كأنها دوال غير متزامنة محلية، مع إكمال تلقائي كامل واستنتاج لنوع القيمة المُعادة:

// apps/web/src/routes/notes.tsx
import { useState } from "react";
import { trpc } from "../utils/trpc";
 
export function NotesPage() {
  const utils = trpc.useUtils();
  const notesQuery = trpc.notes.list.useQuery();
  const createNote = trpc.notes.create.useMutation({
    onSuccess: () => utils.notes.list.invalidate(),
  });
 
  const [title, setTitle] = useState("");
 
  return (
    <div className="mx-auto max-w-xl p-6">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!title.trim()) return;
          createNote.mutate({ title });
          setTitle("");
        }}
        className="flex gap-2"
      >
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="عنوان ملاحظة جديدة"
          className="flex-1 rounded border px-3 py-2"
        />
        <button className="rounded bg-indigo-600 px-4 py-2 text-white">
          إضافة
        </button>
      </form>
 
      <ul className="mt-6 space-y-2">
        {notesQuery.data?.map((note) => (
          <li key={note.id} className="rounded border p-3">
            <strong>{note.title}</strong>
            <p className="text-sm text-gray-500">{note.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

لاحظ ما الذي لم تفعله: لا fetch، ولا واجهة استجابة مكتوبة الأنواع يدويًا، ولا سلاسل لعنوان الـAPI الأساسي، ولا تحويلات as. مرّر المؤشر فوق note في محرّرك فيُبلِغك TypeScript بأنه من النوع Note — النوع المستنتج عينه من الخطوة 4. هذه هي ثمرة المنظومة.

الخطوة 7: إثبات أن أمان الأنواع حقيقي

إليك الاختبار الذي يجعل كل ذلك يستحق العناء. عُد إلى المخطط وأعد تسمية title إلى heading:

// apps/server/src/db/schema/notes.ts
heading: text("heading").notNull(),  // كان: title

دون لمس أي شيء آخر، شغّل فحص الأنواع عبر المستودع الأحادي:

bun run check-types

يُفشل TypeScript عملية البناء ويشير إلى كلٍّ من إجراء الخادم (الذي ما زال يشير إلى notes.title) ومكوّن React (الذي يقرأ note.title). يُكتشف عدم التطابق في وقت الترجمة، داخل المحرّر، قبل إجراء أي طلب. حلقة التغذية الراجعة تلك — عبر حدود الشبكة، في فحص أنواع واحد — هي القيمة الجوهرية التي يقدّمها Better-T-Stack. تراجع عن إعادة التسمية إلى title بعد أن ترى الأخطاء.

الخطوة 8: إضافة المصادقة بـBetter Auth

لأنك أنشأت المشروع بالراية --auth، فإن Better Auth مُهيّأ بالفعل في apps/server/src/lib/auth.ts ومثبّت في تطبيق Hono. لحماية إجراء خلف جلسة، استبدل publicProcedure بالإجراء المُولَّد protectedProcedure:

import { protectedProcedure } from "../lib/trpc";
 
create: protectedProcedure
  .input(z.object({ title: z.string().min(1) }))
  .mutation(async ({ input, ctx }) => {
    // ctx.session.user مكتوب الأنواع ومضمون وجوده هنا
    return db.insert(notes).values({
      title: input.title,
      body: "",
    }).returning();
  });

يتحقّق وسيط protectedProcedure من الجلسة ويطلق خطأ UNAUTHORIZED إن لم يكن المستخدم مسجّل الدخول، لذا يكون ctx.session غير فارغ داخل معالجك. يوفّر Better Auth نقاط نهاية تسجيل الدخول والتسجيل والجلسة؛ ويمنحك عميل الواجهة (authClient) دوال signIn وsignUp وuseSession مكتوبة الأنواع جاهزة.

اختبار تنفيذك

تحقّق من أن الحلقة الكاملة تعمل:

  1. شغّل bun run db:push وتأكّد من إنشاء جدول notes (افتح ملف SQLite عبر bun run db:studio إن كان Drizzle Studio مربوطًا).
  2. شغّل bun dev وافتح تطبيق الويب.
  3. أضف ملاحظة عبر النموذج وتأكّد من ظهورها في القائمة وبقائها بعد تحديث الصفحة (فهي محفوظة في SQLite).
  4. شغّل bun run check-types — يجب أن ينجح بصفر أخطاء عندما يتفق مخططك مع مستهلكيه.

استكشاف الأخطاء وإصلاحها

bun: command not found — ثبّت Bun، ثم أعد تشغيل الطرفية ليُحمّل مسار ~/.bun/bin.

استدعاءات tRPC تُرجع أخطاء CORS — تأكّد من أن أصل CORS في الخادم يطابق رابط تطبيق الويب. يضبط Better-T-Stack متغير البيئة CORS_ORIGIN في apps/server/.env؛ حدّثه إن غيّرت المنافذ.

BETTER_AUTH_SECRET is not defined — أنشئ واحدًا عبر openssl rand -base64 32 وأضفه إلى apps/server/.env.

تغييرات المخطط لا تنعكس — أعد تشغيل bun run db:push. لـSQLite أثناء التطوير المبكر يعيد هذا كتابة الجدول المحلي؛ وللإنتاج استخدم drizzle-kit generate لإنتاج عمليات ترحيل مُرقّمة بدلًا من ذلك.

الأنواع لا تتحدّث في المحرّر — أعد تشغيل خادم TypeScript (في VS Code: لوحة الأوامر ← "TypeScript: Restart TS Server"). يتشارك المستودع الأحادي الأنواع عبر مراجع المشاريع، التي تحتاج أحيانًا إلى دفعة.

الخطوات التالية

  • استبدل SQLite بـPostgreSQL مع مزوّد مُستضاف (Neon أو Supabase أو Turso) عبر إعادة الإنشاء بـ--database postgres --db-setup neon، أو عدّل إعداد Drizzle يدويًا.
  • أضف oRPC بدلًا من tRPC إن أردت توليد OpenAPI إلى جانب أمان الأنواع — يدعمه Better-T-Stack كخيار API بديل.
  • انشر apps/server على Cloudflare Workers باختيار بيئة تشغيل Workers؛ تهيّئ الأداة Wrangler نيابةً عنك.
  • استكشف أدلّتنا ذات الصلة: Drizzle ORM مع Next.js، وtRPC مع موجِّه تطبيق Next.js، والمصادقة بـBetter Auth، وبناء واجهات REST بـHono وBun.

الخاتمة

Better-T-Stack ليس إطار عمل عليك تعلّمه — إنه أداة إنشاء تجمع مكتبات مستقلة من الطراز الأول في مستودع أحادي متماسك وآمن الأنواع من الطرف إلى الطرف. المكسب الحقيقي هو حلقة التغذية الراجعة التي رأيتها في الخطوة 7: تغيير في مخطط قاعدة بياناتك يظهر كخطأ ترجمة في مكوّن React الخاص بك، عبر حدود الشبكة، قبل أن تشغّل أي شيء. اخترت منظومتك على نحو انتقائي، واحتفظت بصفر ارتباط بمزوّد، وأطلقت ميزة full-stack تعمل في جلسة واحدة. من هنا، تصبح كل طبقة — الواجهة الأمامية والـAPI وORM والمصادقة — أداة قياسية يمكنك توسيعها أو استبدالها بشكل مستقل.

المصادر: create-better-t-stack على GitHub، وتوثيق Better-T-Stack، وحزمة npm.