واجهات GraphQL

أفضل ممارسات تصميم المخطط

18 دقيقة الدرس 24 من 35

أهمية تصميم المخطط

مخطط GraphQL الخاص بك هو العقد بين واجهة برمجة التطبيقات الخاصة بك ومستهلكيها. المخططات المصممة جيدًا بديهية ومرنة وقابلة للصيانة. يؤدي تصميم المخطط السيئ إلى تغييرات مدمرة، وواجهات برمجة تطبيقات محيرة، ومطورين محبطين. يضمن اتباع أفضل الممارسات أن يتوسع مخططك مع تطبيقك ويظل مناسبًا للمطورين.

نهج المخطط أولاً مقابل الكود أولاً

نهجان رئيسيان لبناء مخططات GraphQL:

المخطط أولاً (SDL-First)

حدد مخططك باستخدام لغة تعريف مخطط GraphQL (SDL) أولاً، ثم نفذ المحللات:

// schema.graphql type User { id: ID! name: String! email: String! posts: [Post!]! } type Post { id: ID! title: String! content: String! author: User! } type Query { user(id: ID!): User users: [User!]! } // resolvers.js const resolvers = { Query: { user: (parent, { id }, context) => { return context.db.getUserById(id); }, users: (parent, args, context) => { return context.db.getAllUsers(); } }, User: { posts: (user, args, context) => { return context.db.getPostsByUserId(user.id); } } };
الإيجابيات: فصل واضح، سهولة مراجعة تغييرات المخطط، رائع للفرق، مستقل عن اللغة
السلبيات: يمكن أن يصبح المخطط والكود غير متزامنين، يتطلب التحقق اليدوي

الكود أولاً (Resolver-First)

حدد المخطط باستخدام فئات TypeScript/JavaScript والمزخرفات:

import { ObjectType, Field, ID, Resolver, Query, Arg } from 'type-graphql'; @ObjectType() class User { @Field(type => ID) id: string; @Field() name: string; @Field() email: string; @Field(type => [Post]) posts: Post[]; } @ObjectType() class Post { @Field(type => ID) id: string; @Field() title: string; @Field() content: string; @Field(type => User) author: User; } @Resolver(User) class UserResolver { @Query(returns => User, { nullable: true }) user(@Arg('id', type => ID) id: string): Promise<User> { return db.getUserById(id); } @Query(returns => [User]) users(): Promise<User[]> { return db.getAllUsers(); } }
الإيجابيات: أمان النوع، مخطط يتم إنشاؤه تلقائيًا، تكرار أقل، دعم IDE
السلبيات: مرتبط باللغة/الإطار، أصعب في مراجعة المخطط بشكل مستقل
التوصية: استخدم المخطط أولاً لواجهات برمجة التطبيقات العامة أو الفرق الكبيرة حيث تكون مراجعة المخطط حرجة. استخدم الكود أولاً لواجهات برمجة التطبيقات الداخلية أو عندما يكون أمان النوع أمرًا بالغ الأهمية.

اتفاقيات التسمية

تحسن التسمية المتسقة قابلية اكتشاف واجهة برمجة التطبيقات وسهولة استخدامها:

الأنواع والحقول

// ✅ جيد - تسمية واضحة ومتسقة type User { id: ID! firstName: String! # camelCase للحقول lastName: String! emailAddress: String! createdAt: DateTime! # أسماء واضحة وصفية isActive: Boolean! # حقول Boolean تبدأ بـ "is/has/can" } type Post { id: ID! title: String! publishedAt: DateTime # قابل للقيمة null للمسودات author: User! # مفرد للكائنات الفردية comments: [Comment!]! # جمع للقوائم } // ❌ سيئ - تسمية غير متسقة وغير واضحة type user { # يجب أن تكون الأنواع PascalCase ID: ID! # يجب أن تكون الحقول camelCase name: String! # غامض - الاسم الأول؟ الاسم الكامل؟ email_address: String! # Snake_case غير موصى به created: String! # نوع غامض، تنسيق غير واضح active: Boolean! # بادئة "is" مفقودة Author: User! # يجب أن يكون الحقل camelCase }

الاستعلامات والطفرات

// ✅ جيد - أفعال عمل واضحة type Query { user(id: ID!): User # مفرد لعنصر واحد users(limit: Int, offset: Int): [User!]! # جمع للقوائم searchUsers(query: String!): [User!]! # فعل وصفي + اسم } type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload! deleteUser(id: ID!): DeleteUserPayload! publishPost(id: ID!): PublishPostPayload! # فعل عمل واضح } // ❌ سيئ - تسمية غامضة أو غير متسقة type Query { getUser(id: ID!): User # "get" زائد في الاستعلامات allUsers: [User!]! # غير متسق مع "user" المفرد search(q: String!): [User!]! # عام جدًا، غير واضح ما يتم البحث عنه } type Mutation { user(input: UserInput!): User! # غير واضح إذا كان إنشاء/تحديث remove(id: ID!): Boolean! # عام جدًا، ما الذي يتم إزالته؟ post(id: ID!): Post! # إجراء غير واضح }

أنواع الإدخال والوسائط

// ✅ جيد - أنواع إدخال واضحة قابلة لإعادة الاستخدام input CreateUserInput { firstName: String! lastName: String! email: String! } input UpdateUserInput { firstName: String lastName: String email: String } input PaginationInput { limit: Int = 10 offset: Int = 0 } type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload! } // ❌ سيئ - مدخلات متكررة وغير واضحة type Mutation { createUser( firstName: String!, lastName: String!, email: String! ): User! # قوائم الوسائط الطويلة يصعب صيانتها updateUser( id: ID!, data: UserData! # اسم عام، غرض غير واضح ): User! }

أنواع الاستجابة ومعالجة الأخطاء

أرجع دائمًا أنواع حمولة منظمة بدلاً من الكائنات المباشرة:

// ✅ جيد - استجابة منظمة مع بيانات وصفية type CreateUserPayload { success: Boolean! message: String user: User errors: [UserError!] } type UserError { field: String message: String! code: ErrorCode! } enum ErrorCode { VALIDATION_ERROR DUPLICATE_EMAIL INVALID_INPUT UNAUTHORIZED } type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! } // الاستخدام يسمح بالنجاح الجزئي والأخطاء المفصلة // { // "data": { // "createUser": { // "success": false, // "message": "فشل التحقق", // "user": null, // "errors": [ // { // "field": "email", // "message": "البريد الإلكتروني موجود بالفعل", // "code": "DUPLICATE_EMAIL" // } // ] // } // } // } // ❌ سيئ - إرجاع كائن مباشر، أخطاء في الامتدادات type Mutation { createUser(input: CreateUserInput!): User! } // لا توجد طريقة لإرجاع بيانات جزئية أو أخطاء منظمة // يجب أن تذهب الأخطاء إلى مصفوفة الأخطاء على المستوى الأعلى
تحذير: رمي الأخطاء في المحللات يتسبب في فشل العملية بأكملها. استخدم أنواع حمولة منظمة لإرجاع بيانات جزئية وأخطاء خاصة بالحقل.

أنماط الترقيم

نفذ ترقيمًا متسقًا عبر مخططك:

// ترقيم المؤشر بنمط Relay (موصى به لمجموعات البيانات الكبيرة) type Query { users(first: Int, after: String, last: Int, before: String): UserConnection! } type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! } type UserEdge { cursor: String! node: User! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } // ترقيم الإزاحة (أبسط، جيد لمجموعات البيانات الصغيرة) type Query { users(limit: Int = 10, offset: Int = 0): UserList! } type UserList { items: [User!]! totalCount: Int! limit: Int! offset: Int! }

إصدار المخطط وتطوره

يجب أن تتطور مخططات GraphQL دون تغييرات مدمرة:

إضافة الحقول (آمن)

// الإصدار 1 type User { id: ID! name: String! } // الإصدار 2 - إضافة حقل اختياري جديد (غير مدمر) type User { id: ID! name: String! email: String # حقل اختياري جديد }

إهمال الحقول

type User { id: ID! name: String! @deprecated(reason: "استخدم firstName و lastName بدلاً من ذلك") firstName: String! lastName: String! # الحقل القديم لا يزال يعمل، لكن العملاء يتم تحذيرهم age: Int @deprecated(reason: "استخدم birthDate لحساب العمر الدقيق") birthDate: DateTime! } type Query { users: [User!]! @deprecated(reason: "استخدم searchUsers مع الترقيم") searchUsers( query: String, first: Int, after: String ): UserConnection! }
استراتيجية الإهمال:
  1. أضف توجيه @deprecated مع مسار ترحيل واضح
  2. راقب استخدام الحقول المهملة
  3. تواصل مع جدول زمني لإنهاء العمل مع مستهلكي واجهة برمجة التطبيقات
  4. أزل الحقول المهملة في الإصدار الرئيسي التالي

التغييرات المدمرة التي يجب تجنبها

// ❌ مدمر: إزالة حقل type User { id: ID! # name: String! # تمت الإزالة - يكسر الاستعلامات الموجودة } // ❌ مدمر: تغيير نوع الحقل type User { id: ID! age: String! # تغير من Int! إلى String! - يكسر العملاء } // ❌ مدمر: جعل الحقل الاختياري مطلوبًا type User { id: ID! email: String! # تغير من String إلى String! - يكسر الطفرات } // ❌ مدمر: تغيير متطلبات الوسيطة type Query { user(id: ID!, email: String!): User # تمت إضافة وسيطة مطلوبة - يكسر الاستعلامات } // ✅ آمن: استراتيجية التطور type User { id: ID! name: String! @deprecated(reason: "استخدم displayName") displayName: String! # أضف حقلاً جديدًا أولاً age: Int @deprecated(reason: "استخدم birthDate") birthDate: DateTime # أضف بديلاً اختياريًا }

تقسيم المخططات الكبيرة إلى وحدات

قسم المخططات الكبيرة إلى وحدات منطقية:

// schema/user.graphql type User { id: ID! name: String! posts: [Post!]! } extend type Query { user(id: ID!): User users: [User!]! } extend type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! } // schema/post.graphql type Post { id: ID! title: String! author: User! } extend type Query { post(id: ID!): Post posts: [Post!]! } extend type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! } // schema/index.js const { mergeTypeDefs } = require('@graphql-tools/merge'); const { loadFilesSync } = require('@graphql-tools/load-files'); const path = require('path'); const typesArray = loadFilesSync(path.join(__dirname, './'), { extensions: ['graphql'] }); const typeDefs = mergeTypeDefs(typesArray); module.exports = typeDefs;

توثيق المخطط

وثق مخططك بالأوصاف:

""" يمثل مستخدمًا في النظام. يمكن للمستخدمين إنشاء منشورات وتعليقات والتفاعل مع مستخدمين آخرين. """ type User { """معرف فريد للمستخدم""" id: ID! """اسم العرض الكامل للمستخدم""" name: String! """ عنوان البريد الإلكتروني للمستخدم. هذا الحقل مرئي فقط للمستخدم نفسه والمسؤولين. """ email: String! """ جميع المنشورات التي أنشأها هذا المستخدم. النتائج مرقمة ومرتبة حسب تاريخ الإنشاء (الأحدث أولاً). """ posts(first: Int = 10, after: String): PostConnection! """الطابع الزمني عند إنشاء حساب المستخدم""" createdAt: DateTime! } """ إدخال لإنشاء حساب مستخدم جديد. جميع الحقول مطلوبة أثناء تسجيل المستخدم. """ input CreateUserInput { """الاسم الكامل للمستخدم (2-100 حرفًا)""" name: String! """عنوان بريد إلكتروني صالح (يجب أن يكون فريدًا)""" email: String! """كلمة المرور (8 أحرف على الأقل، يجب أن تتضمن أحرفًا وأرقامًا)""" password: String! }
نصيحة: استخدم علامات اقتباس ثلاثية (""") للأوصاف متعددة الأسطر. تظهر الأوصاف في GraphQL Playground وأدوات التوثيق والإكمال التلقائي لـ IDE.

التحقق من صحة المخطط والتحليل

استخدم الأدوات لفرض أفضل ممارسات المخطط:

// .graphql-config.yml schema: "src/schema/**/*.graphql" documents: "src/**/*.{graphql,js,ts}" extensions: validation: rules: - naming-convention: types: PascalCase fields: camelCase arguments: camelCase enums: UPPER_CASE - no-deprecated - require-description - no-typename-prefix // package.json { "scripts": { "lint:schema": "graphql-schema-linter src/schema/**/*.graphql", "validate:schema": "graphql-inspector validate 'src/schema/**/*.graphql'" } } // تشغيل التحليل npm run lint:schema // المخرجات: // ✅ جميع الأنواع تتبع PascalCase // ❌ الحقل "User.Email" يجب أن يكون camelCase // ❌ النوع "Post" يفتقد إلى الوصف
تمرين: صمم مخطط GraphQL كامل لمنصة تجارة إلكترونية باتباع أفضل الممارسات:
  1. أنشئ أنواعًا لـ Product وOrder وCustomer وReview مع تسمية مناسبة
  2. نفذ ترقيمًا قائمًا على المؤشر لقوائم المنتجات
  3. صمم أنواع حمولة منظمة لجميع الطفرات
  4. أضف أوصافًا على مستوى الحقل لجميع الأنواع
  5. قم بتضمين استراتيجية إهمال لإعادة تسمية "Customer" إلى "User"
  6. قسم المخطط إلى ملفات وحدة حسب المجال
  7. أضف رموز أخطاء شاملة وأنواع أخطاء
وثق قرارات التصميم واستراتيجية التطور الخاصة بك.