Token导航 LogoToken导航

Agent Skill · 开发规范

apollo-graphql-best-practices(apollo GraphQL 最佳实践)

用于辅助 API 设计、接口文档、请求响应结构和服务集成说明。它适合让 Agent 梳理 endpoint、生成 OpenAPI 草稿、检查字段命名、整理错误码或辅助前后端联调。使用时需要确认真实业务语义、鉴权方式、分页和错误处理规则;涉及生成接口文档时,应避免凭空补字段,最好从现有代码、schema 或接口样例中提取事实。

apollo-graphql-best-practices 是开发规范类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证为 MIT,第三方扫描已通过,按安装量在开发规范类的 1,265 个 Skill 里排第 540。

总安装 1,028周安装 42GitHub Stars 公开资料未说明下载 329核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
MIT

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:apollo-graphql-best-practices(apollo GraphQL 最佳实践)
来源仓库:https://github.com/maximepzv/skills
仓库路径:skills/apollo-graphql-best-practices
安装命令:
npx skills add https://github.com/maximepzv/skills --skill apollo-graphql-best-practices
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/maximepzv/skills --skill apollo-graphql-best-practices

这个 Skill 是做什么的

开发规范类 Skill 用来统一代码风格、目录结构和评审口径,产出多为检查清单或改写建议。

apollo-graphql-best-practices 用于辅助 API 设计、接口文档和请求响应结构梳理,适合生成 OpenAPI 草稿和前后端联调支持。它能帮助检查字段命名、错误码整理和服务集成说明,适用于接口开发阶段。

使用时需确认真实业务语义和鉴权方式,避免凭空补字段;最好从现有代码或 schema 中提取事实。安装前建议确认权限范围和维护状态,注意是否会触发文件读写或网络请求。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

来源仓库内路径:skills/apollo-graphql-best-practices

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/maximepzv/skills --skill apollo-graphql-best-p…;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

apollo-graphql-best-practices 怎么安装?

在终端执行 npx skills add https://github.com/maximepzv/skills --skill apollo-graphql-best-practices。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

apollo-graphql-best-practices 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

apollo-graphql-best-practices 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

apollo-graphql-best-practices 是免费的吗?

来源标注的许可证是 MIT。本站不代收任何费用,也不托管安装包;能否商用、要不要署名,以许可证原文为准。

apollo-graphql-best-practices 用的人多吗?

公开数据是累计安装 1,028 次,近一周安装 42 次,下载 329 次。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

apollo-graphql-best-practices 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 3 个目录同时收录,说明这个 Skill 流传比较广。
  • 许可证明确为 MIT,商用和分发边界好判断。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理开发规范类任务、又不想每次都手写提示词的人。
  • 需要许可证清楚、并且有第三方扫描结果的团队项目。

不太适合

  • 需要厂商级技术支持和 SLA 的生产系统。

开发规范类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
vercel-react-best-practices

Vercel React 最佳实践 · Codex、Claude

8,490,72025,918操作浏览器MIT通过
web-design-guidelines

网页设计指南 · Codex、Claude

6,686,01625,885需要联网MIT有提醒
remotion-best-practices

Remotion 最佳实践 · Codex、Claude

6,505,6323,027操作浏览器MIT有提醒
supabase-postgres-best-practices

Supabase Postgres 最佳实践 · Codex、Claude

3,175,5232,030需要联网MIT通过
next-best-practices

Next.js 最佳实践 · Codex、Claude

1,848,240841操作浏览器MIT通过
apollo-graphql-best-practices

apollo GraphQL 最佳实践 · 本页

1,028—需要联网MIT通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Apollo GraphQL Best Practices

When to Apply

Use this skill when:

  • Setting up a new Apollo Client or Apollo Server project
  • Writing GraphQL queries, mutations, or subscriptions
  • Implementing React components with useQuery, useMutation, or useLazyQuery
  • Configuring cache policies, type policies, or pagination
  • Designing GraphQL schemas or writing resolvers
  • Handling GraphQL or network errors
  • Optimizing GraphQL performance (N+1 queries, caching, batching)
  • Reviewing or refactoring existing Apollo GraphQL code

Apollo Client

Client Setup

Configure ApolloClient with InMemoryCache and appropriate type policies:

import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";

const client = new ApolloClient({
  link: new HttpLink({ uri: "/graphql" }),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          // Define field policies for pagination, merging, etc.
        },
      },
      // Custom key fields for entity identification
      User: {
        keyFields: ["email"], // Use email instead of id
      },
    },
  }),
});

Queries with useQuery

import { useQuery, gql } from "@apollo/client";

const GET_DATA = gql`
  query GetData($id: ID!) {
    item(id: $id) {
      id
      name
    }
  }
`;

function Component({ id }: { id: string }) {
  const { data, loading, error } = useQuery(GET_DATA, {
    variables: { id },
    fetchPolicy: "cache-first", // Default, use cache when available
  });

  if (loading) return <Loading />;
  if (error) return <Error message={error.message} />;
  return <Display data={data} />;
}

Mutations with useMutation

Update cache after mutations using update callback:

import { useMutation, gql } from "@apollo/client";

const ADD_ITEM = gql`
  mutation AddItem($input: ItemInput!) {
    addItem(input: $input) {
      id
      name
    }
  }
`;

function AddItemForm() {
  const [addItem, { loading }] = useMutation(ADD_ITEM, {
    update(cache, { data: { addItem } }) {
      cache.modify({
        fields: {
          items(existingItems = []) {
            const newItemRef = cache.writeFragment({
              data: addItem,
              fragment: gql`
                fragment NewItem on Item {
                  id
                  name
                }
              `,
            });
            return [...existingItems, newItemRef];
          },
        },
      });
    },
    // Or use refetchQueries for simpler cases
    // refetchQueries: [{ query: GET_ITEMS }],
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      addItem({ variables: { input: { name: "New Item" } } });
    }}>
      <button type="submit" disabled={loading}>Add</button>
    </form>
  );
}

Error Handling

GraphQL can return partial data with errors (unlike REST where a single error fails the entire request). Use errorPolicy to control this behavior:

PolicyBehavior
noneTreat any GraphQL error as a network error, discard data (default)
ignoreIgnore GraphQL errors, return only data
allReturn both data and errors, enabling partial data rendering
import { useQuery } from "@apollo/client";

function Component() {
  const { data, error } = useQuery(QUERY, {
    errorPolicy: "all" // Receive partial data with errors
  });

  if (error) {
    // Check if we have partial data to display
    if (data) {
      // Render partial data with error notification
      return (
        <div>
          <ErrorBanner message={error.message} />
          <Display data={data} />
        </div>
      );
    }
    // No data at all - show full error
    if (error.networkError) {
      return <div>Network error: {error.message}</div>;
    }
    return <div>Error: {error.graphQLErrors[0]?.message}</div>;
  }

  return <div>{data?.field}</div>;
}

Cache Type Policies

Configure cache normalization, custom identifiers, and field merging:

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        // Offset-based pagination with proper merging
        items: {
          keyArgs: ["filter"], // Cache separately per filter value
          merge(existing = [], incoming, { args }) {
            const offset = args?.offset ?? 0;
            const merged = existing.slice(0);
            for (let i = 0; i < incoming.length; i++) {
              merged[offset + i] = incoming[i];
            }
            return merged;
          },
        },
      },
    },
    // Custom cache key using different field than 'id'
    User: {
      keyFields: ["email"], // Use email as unique identifier
    },
    // Entities without id field
    Token: {
      keyFields: false, // Treat as singleton (no normalization)
    },
    // Composite key for join tables
    OrderItem: {
      keyFields: ["orderId", "productId"],
    },
  },
});

Key concepts:

  • Cache normalization: Apollo stores objects in a flat lookup table using __typename:id as the cache key
  • keyFields: Customize which fields identify an entity (default is id or _id)
  • keyArgs: Control which arguments create separate cache entries
  • merge: Define how to combine existing and incoming data (essential for pagination)

Fetch Policies

PolicyBehavior
cache-firstRead cache, fetch if missing (default)
cache-onlyOnly read cache, never fetch
network-onlyAlways fetch, update cache
no-cacheAlways fetch, don't cache
cache-and-networkReturn cache immediately, then fetch

Apollo Server

Server Setup

import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

interface Context {
  user?: User;
  db: Database;
}

const server = new ApolloServer<Context>({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({
    user: await getUserFromToken(req.headers.authorization),
    db: await getDatabase(),
  }),
  listen: { port: 4000 },
});

Schema Design Principles

  1. Use non-nullable by default - Add ! unless field can legitimately be null
  2. Prefer specific types - Use ID! for identifiers, custom scalars for dates
  3. Design for the client - Structure schema around UI needs, not database schema
  4. Use input types for mutations - Group related arguments
type Query {
  user(id: ID!): User
  users(filter: UserFilter, pagination: Pagination): UserConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
  updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}

input CreateUserInput {
  email: String!
  name: String!
}

type CreateUserPayload {
  user: User
  errors: [Error!]
}

Resolvers

const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      return context.db.users.findById(id);
    },
  },
  Mutation: {
    createUser: async (_, { input }, context) => {
      if (!context.user) {
        throw new GraphQLError("Not authenticated", {
          extensions: { code: "UNAUTHENTICATED" },
        });
      }
      const user = await context.db.users.create(input);
      return { user, errors: [] };
    },
  },
  // Field resolvers for computed/related data
  User: {
    posts: (parent, _, context) => {
      return context.db.posts.findByUserId(parent.id);
    },
  },
};

Error Handling

Throw GraphQLError with descriptive codes:

import { GraphQLError } from "graphql";

// In resolver
if (!user) {
  throw new GraphQLError("User not found", {
    extensions: {
      code: "NOT_FOUND",
      argumentName: "id",
    },
  });
}

// In context for auth errors
context: async ({ req }) => {
  const user = await getUser(req);
  if (!user) {
    throw new GraphQLError("Authentication required", {
      extensions: {
        code: "UNAUTHENTICATED",
        http: { status: 401 },
      },
    });
  }
  return { user };
};

Standard Error Codes

CodeUse Case
UNAUTHENTICATEDMissing or invalid authentication
FORBIDDENAuthenticated but not authorized
BAD_USER_INPUTInvalid argument values
NOT_FOUNDRequested resource doesn't exist
INTERNAL_SERVER_ERRORUnexpected server errors

Performance Tips

  1. Use DataLoader - Batch and cache database calls to avoid N+1 queries
  2. Implement pagination - Never return unbounded lists
  3. Use persisted queries - Reduce request size in production
  4. Enable APM - Use Apollo Studio for query performance monitoring
  5. Lazy load fragments - Split large queries with @defer directive
  6. Configure cache TTL - Set appropriate maxAge for cached responses
  7. Limit query depth - Prevent deeply nested queries that can cause performance issues
  8. Set query complexity limits - Assign costs to fields and reject overly complex queries

Security Best Practices

  1. Query depth limiting - Prevent malicious deeply nested queries
  2. Query complexity analysis - Assign costs to fields, reject queries exceeding threshold
  3. Rate limiting - Throttle requests per client/IP
  4. Disable introspection in production - Hide schema from unauthorized users
  5. Input validation - Validate all user inputs in resolvers
  6. Field-level authorization - Check permissions in field resolvers, not just at query level
// Example: formatError to hide internal errors in production
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== "production",
  formatError: (error) => {
    // Log full error internally
    console.error(error);

    // Don't expose internal errors to clients
    if (error.extensions?.code === "INTERNAL_SERVER_ERROR") {
      return new GraphQLError("Internal server error", {
        extensions: { code: "INTERNAL_SERVER_ERROR" },
      });
    }
    return error;
  },
});

Relay-Style Patterns

Global IDs

Use base64-encoded type:uuid format for globally unique identifiers. This follows the Relay Global Object Identification Specification.

Benefits:

  • Enables efficient client-side caching and data refetching
  • Allows node(id: ID!) query to fetch any entity by ID
  • Type information embedded in ID prevents accidental cross-type queries
  • Libraries like Relay can automatically generate pagination and refetch queries
// utils/globalId.ts
export type GlobalIdType = "user" | "request" | "classified" | "purchase";

export function encodeGlobalId(type: GlobalIdType, uuid: string): string {
  return Buffer.from(`${type}:${uuid}`).toString("base64");
}

export function decodeGlobalId(globalId: string): { type: string; uuid: string } {
  const decoded = Buffer.from(globalId, "base64").toString("utf-8");
  const [type, uuid] = decoded.split(":");

  if (!type || !uuid) {
    throw new Error("Invalid global ID format");
  }

  return { type, uuid };
}

// Examples
encodeGlobalId("request", "abc-123"); // -> "cmVxdWVzdDphYmMtMTIz"
decodeGlobalId("cmVxdWVzdDphYmMtMTIz"); // -> { type: 'request', uuid: 'abc-123' }

Node Interface

Implement the Node interface for unified entity fetching. This is a core pattern from the Relay specification that provides:

  • A standard way to refetch any object by its ID
  • Efficient cache management for client libraries
  • Type-safe polymorphic queries
# schema/node.graphql
interface Node {
  id: ID!
}

extend type Query {
  """
  Fetch any entity by its global ID.
  Returns null if the ID is invalid or the entity doesn't exist.
  """
  node(id: ID!): Node
}
// resolvers/node.ts
export const nodeResolvers = {
  Query: {
    async node(parent: any, { id }: { id: string }, context: GraphQLContext) {
      try {
        const { type, uuid } = decodeGlobalId(id);

        switch (type) {
          case "request":
            return context.loaders.request.load(uuid);
          case "classified":
            return context.loaders.classified.load(uuid);
          case "purchase":
            return context.loaders.purchase.load(uuid);
          default:
            return null;
        }
      } catch (error) {
        return null; // Silent failure for invalid IDs
      }
    },
  },

  Node: {
    __resolveType(obj: any) {
      // Detect type based on unique fields or __typename
      if (obj.__typename) return obj.__typename;
      if ("condition" in obj && "priceMinimum" in obj) return "Request";
      if ("platformConfigId" in obj && "visibilityStatus" in obj) return "Classified";
      if ("buyerEmail" in obj || "shippingAddress" in obj) return "Purchase";
      return null;
    },
  },
};

Entity Type with Global ID

type Request implements Node {
  id: ID!  # Global ID (base64 encoded)
  title: String!
  description: String!
  # ... other fields
}
// resolvers/request.ts
Request: {
  // Encode database UUID to global ID
  id(request: any) {
    return encodeGlobalId("request", request.id);
  },
}

DataLoaders for Batching

Essential for efficient Node queries and avoiding N+1 problems. The N+1 problem occurs when fetching a list of items (1 query), then fetching related data for each item individually (N queries).

Key DataLoader principles:

  • Create fresh DataLoaders per request (never share across requests)
  • Always return results in the same order as input keys
  • Use caching within a single request to deduplicate identical fetches
  • Set maxBatchSize to prevent overly large queries
// dataloaders/request.loader.ts
import DataLoader from "dataloader";

export function createRequestLoader(db: Database) {
  return new DataLoader<string, any>(
    async (uuids) => {
      // Batch fetch all requested entities in a single query
      // e.g., SELECT * FROM requests WHERE id IN (uuid1, uuid2, ...)
      const results = await db.requests.findByIds([...uuids]);

      // CRITICAL: Return results in same order as input uuids
      // DataLoader requires 1:1 mapping between keys and results
      const resultMap = new Map(results.map((r) => [r.id, r]));
      return uuids.map((uuid) => resultMap.get(uuid) ?? null);
    },
    {
      cache: true,        // Cache within this request
      maxBatchSize: 100,  // Limit batch size for DB query performance
    }
  );
}
// dataloaders/index.ts
export interface Loaders {
  request: DataLoader<string, any>;
  classified: DataLoader<string, any>;
  purchase: DataLoader<string, any>;
}

export function createLoaders(db: Database): Loaders {
  return {
    request: createRequestLoader(db),
    classified: createClassifiedLoader(db),
    purchase: createPurchaseLoader(db),
  };
}

Context with DataLoaders

Create fresh DataLoaders per request:

// server.ts
export interface GraphQLContext {
  user: User | null;
  permissions: Permissions;
  db: Database;
  loaders: Loaders;
}

app.use(
  "/graphql",
  expressMiddleware(apolloServer, {
    context: async ({ req }): Promise<GraphQLContext> => ({
      user: (req as any).connectedUser ?? null,
      permissions: (req as any).connectedUserPermissions,
      db: database,
      loaders: createLoaders(database), // Fresh loaders per request
    }),
  })
);

Offset-Based Pagination (Connection Pattern)

# schema/common.graphql
type PageInfo {
  total: Int!
  offset: Int!
  limit: Int!
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
}

input PaginationInput {
  offset: Int = 0
  limit: Int = 50
}

type RequestConnection {
  items: [Request!]!
  pageInfo: PageInfo!
}

extend type Query {
  requests(pagination: PaginationInput): RequestConnection!
}
// resolvers/request.ts
async requests(
  parent: any,
  { pagination = {} }: { pagination?: { offset?: number; limit?: number } },
  context: GraphQLContext
) {
  const { offset = 0, limit = 50 } = pagination;

  const [items, total] = await Promise.all([
    context.db.requests.findMany({
      where: { deletedAt: null },
      skip: offset,
      take: limit,
      orderBy: { createdAt: "asc" },
    }),
    context.db.requests.count({ where: { deletedAt: null } }),
  ]);

  return {
    items,
    pageInfo: {
      total,
      offset,
      limit,
      hasNextPage: offset + limit < total,
      hasPreviousPage: offset > 0,
    },
  };
}

Silent Permission Handling (Queries)

For queries, return null instead of throwing errors:

async request(parent: any, { id }: { id: string }, context: GraphQLContext) {
  try {
    const { type, uuid } = decodeGlobalId(id);

    // Wrong type - return null silently
    if (type !== "request") {
      return null;
    }

    // No permission - return null silently
    if (!context.permissions?.canViewRequest?.(uuid)) {
      return null;
    }

    const request = await context.loaders.request.load(uuid);
    return request ?? null;
  } catch (error) {
    console.error("Error fetching request:", error);
    return null;
  }
}

Query Examples

# Fetch single entity by global ID
query GetRequest {
  request(id: "cmVxdWVzdDphYmMxMjM=") {
    id
    title
    description
  }
}

# Using node query for any entity
query GetNode {
  node(id: "cmVxdWVzdDphYmMxMjM=") {
    id
    ... on Request {
      title
      price
    }
    ... on Classified {
      status
      url
    }
  }
}

# Paginated list
query ListRequests {
  requests(pagination: { offset: 0, limit: 10 }) {
    items {
      id
      title
    }
    pageInfo {
      total
      hasNextPage
    }
  }
}

平台分布

Claude Code

27.6%
按下载量换算91

OpenCode

23.01%
按下载量换算76

Gemini CLI

17.7%
按下载量换算58

Antigravity

12.5%
按下载量换算41

Codex

7.93%
按下载量换算26

github-copilot

3.19%
按下载量换算10

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

继续浏览同类 Skills