零度AI
高级20 分钟阅读

Claude Code GraphQL集成

使用 Claude Code 搭建 GraphQL API,替代 RESTful API 提供更灵活的数据查询

Claude CodeGraphQLAPIApollo数据查询

GraphQL 概述

GraphQL 是一种 API 查询语言,允许客户端精确指定需要的数据。Claude Code 可以帮你快速搭建 GraphQL API。

GraphQL vs REST

REST 返回固定的数据结构,GraphQL 让客户端决定需要什么字段,减少数据传输量。

Apollo Server 搭建

项目初始化

bash
帮我创建一个 GraphQL 服务器,使用 Apollo Server 和 Express。
javascript
// lib/graphql/index.js
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { makeExecutableSchema } from "@graphql-tools/schema";
import typeDefs from "./typeDefs.js";
import resolvers from "./resolvers.js";
import authMiddleware from "./auth.js";

export async function createApolloServer() {
  const schema = makeExecutableSchema({ typeDefs, resolvers });

  const server = new ApolloServer({
    schema,
    formatError: (formattedError) => {
      // 生产环境不暴露内部错误
      if (process.env.NODE_ENV === "production") {
        return {
          ...formattedError,
          message: "服务器内部错误",
        };
      }
      return formattedError;
    },
  });

  await server.start();

  return server;
}

export async function applyGraphQLMiddleware(app, server) {
  app.use(
    "/graphql",
    expressMiddleware(server, {
      context: async ({ req }) => {
        const user = await authMiddleware(req);
        return { user };
      },
    })
  );
}

TypeDefs 定义

javascript
// lib/graphql/typeDefs.js
const typeDefs = `#graphql
  scalar DateTime

  type User {
    id: ID!
    username: String!
    email: String!
    avatar: String
    bio: String
    posts: [Post!]!
    postCount: Int!
    followers: [User!]!
    following: [User!]!
    followerCount: Int!
    followingCount: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Post {
    id: ID!
    title: String!
    slug: String!
    content: String
    excerpt: String
    coverImage: String
    author: User!
    tags: [String!]!
    comments: [Comment!]!
    commentCount: Int!
    viewCount: Int!
    isPublished: Boolean!
    publishedAt: DateTime
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Comment {
    id: ID!
    content: String!
    author: User!
    post: Post!
    parent: Comment
    replies: [Comment!]!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type AuthPayload {
    token: String!
    user: User!
  }

  input CreateUserInput {
    username: String!
    email: String!
    password: String!
  }

  input UpdateUserInput {
    username: String
    email: String
    avatar: String
    bio: String
  }

  input CreatePostInput {
    title: String!
    content: String!
    slug: String
    tags: [String!]
    coverImage: String
  }

  input CreateCommentInput {
    postId: ID!
    content: String!
    parentId: ID
  }

  type Query {
    # 用户
    me: User
    user(id: ID!): User
    userByUsername(username: String!): User
    users(limit: Int, offset: Int): [User!]!

    # 文章
    post(id: ID!): Post
    postBySlug(slug: String!): Post
    posts(limit: Int, offset: Int, tag: String, authorId: ID): [Post!]!
    myPosts: [Post!]!

    # 评论
    comments(postId: ID!): [Comment!]!
  }

  type Mutation {
    # 认证
    register(input: CreateUserInput!): AuthPayload!
    login(email: String!, password: String!): AuthPayload!

    # 用户
    updateProfile(input: UpdateUserInput!): User!
    follow(userId: ID!): User!
    unfollow(userId: ID!): User!

    # 文章
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: CreatePostInput!): Post!
    deletePost(id: ID!): Boolean!
    publishPost(id: ID!): Post!
    unpublishPost(id: ID!): Post!

    # 评论
    createComment(input: CreateCommentInput!): Comment!
    deleteComment(id: ID!): Boolean!
  }
`;

export default typeDefs;

Resolvers 实现

javascript
// lib/graphql/resolvers.js
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { DateTime } from "graphql-scalars";

const resolvers = {
  DateTime,

  Query: {
    // 当前用户
    me: async (_, __, { user }) => {
      if (!user) return null;
      return User.findById(user.id);
    },

    // 获取用户
    user: async (_, { id }) => {
      return User.findById(id);
    },

    userByUsername: async (_, { username }) => {
      return User.findOne({ username });
    },

    // 获取文章
    post: async (_, { id }) => {
      return Post.findById(id);
    },

    postBySlug: async (_, { slug }) => {
      return Post.findOne({ slug });
    },

    posts: async (_, { limit = 10, offset = 0, tag, authorId }) => {
      const query = { isPublished: true };
      if (tag) query.tags = { $in: [tag] };
      if (authorId) query.author = authorId;

      return Post.find(query)
        .sort({ publishedAt: -1 })
        .skip(offset)
        .limit(limit);
    },

    myPosts: async (_, __, { user }) => {
      if (!user) throw new Error("请先登录");
      return Post.find({ author: user.id }).sort({ createdAt: -1 });
    },
  },

  Mutation: {
    // 注册
    register: async (_, { input }) => {
      const { username, email, password } = input;

      // 检查用户是否存在
      const existingUser = await User.findOne({ $or: [{ email }, { username }] });
      if (existingUser) {
        throw new Error("用户名或邮箱已被注册");
      }

      // 创建用户
      const hashedPassword = await bcrypt.hash(password, 10);
      const user = await User.create({
        username,
        email,
        password: hashedPassword,
      });

      // 生成 Token
      const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
        expiresIn: "7d",
      });

      return { token, user };
    },

    // 登录
    login: async (_, { email, password }) => {
      const user = await User.findOne({ email }).select("+password");
      if (!user) {
        throw new Error("用户不存在");
      }

      const isValid = await bcrypt.compare(password, user.password);
      if (!isValid) {
        throw new Error("密码错误");
      }

      const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
        expiresIn: "7d",
      });

      return { token, user };
    },

    // 创建文章
    createPost: async (_, { input }, { user }) => {
      if (!user) throw new Error("请先登录");

      const slug = input.slug || generateSlug(input.title);

      const post = await Post.create({
        ...input,
        slug,
        author: user.id,
      });

      return post.populate("author");
    },

    // 发布文章
    publishPost: async (_, { id }, { user }) => {
      if (!user) throw new Error("请先登录");

      const post = await Post.findOneAndUpdate(
        { _id: id, author: user.id },
        { isPublished: true, publishedAt: new Date() },
        { new: true }
      ).populate("author");

      if (!post) throw new Error("文章不存在或无权操作");
      return post;
    },

    // 评论
    createComment: async (_, { input }, { user }) => {
      if (!user) throw new Error("请先登录");

      const { postId, content, parentId } = input;

      const comment = await Comment.create({
        content,
        post: postId,
        author: user.id,
        parent: parentId || null,
      });

      // 更新文章评论数
      await Post.findByIdAndUpdate(postId, { $inc: { commentCount: 1 } });

      return comment.populate(["author", "post", "parent"]);
    },
  },

  // 字段解析
  User: {
    posts: async (user) => {
      return Post.find({ author: user.id, isPublished: true });
    },
    postCount: async (user) => {
      return Post.countDocuments({ author: user.id, isPublished: true });
    },
    followerCount: async (user) => {
      return user.followers?.length || 0;
    },
    followingCount: async (user) => {
      return user.following?.length || 0;
    },
  },

  Post: {
    author: async (post) => {
      return User.findById(post.author);
    },
    comments: async (post) => {
      return Comment.find({ post: post.id, parent: null }).sort({
        createdAt: -1,
      });
    },
  },

  Comment: {
    author: async (comment) => {
      return User.findById(comment.author);
    },
    post: async (comment) => {
      return Post.findById(comment.post);
    },
    replies: async (comment) => {
      return Comment.find({ parent: comment.id }).sort({ createdAt: 1 });
    },
  },
};

function generateSlug(title) {
  return title
    .toLowerCase()
    .replace(/[^\w\s-]/g, "")
    .replace(/\s+/g, "-")
    .substring(0, 50);
}

export default resolvers;

Express 集成

javascript
// app.js
import express from "express";
import cors from "cors";
import { createApolloServer, applyGraphQLMiddleware } from "./lib/graphql/index.js";

const app = express();

// CORS
app.use(cors({
  origin: process.env.CLIENT_URL,
  credentials: true,
}));

const server = await createApolloServer();
await applyGraphQLMiddleware(app, server);

app.listen(4000, () => {
  console.log("GraphQL server running at http://localhost:4000/graphql");
});

前端集成

React + Apollo Client

bash
帮我创建一个 Apollo Client 的配置和使用示例。
javascript
// lib/apollo/client.js
import { ApolloClient, InMemoryCache, createHttpLink, from } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { onError } from "@apollo/client/link/error";

const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || "http://localhost:4000/graphql",
});

// 认证链接
const authLink = setContext((_, { headers }) => {
  const token = typeof window !== "undefined" ? localStorage.getItem("token") : "";

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

// 错误处理
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);

      if (message === "请先登录") {
        // 跳转到登录页
        localStorage.removeItem("token");
        window.location.href = "/login";
      }
    });
  }

  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

export const apolloClient = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            keyArgs: false,
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
      User: {
        fields: {
          posts: {
            merge(existing = [], incoming) {
              return incoming;
            },
          },
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "cache-and-network",
    },
  },
});

React Hooks

jsx
// hooks/useGraphQL.js
import { gql } from "@apollo/client";

// 查询当前用户
export const ME_QUERY = gql`
  query Me {
    me {
      id
      username
      email
      avatar
      bio
    }
  }
`;

// 查询文章列表
export const POSTS_QUERY = gql`
  query Posts($limit: Int, $offset: Int, $tag: String) {
    posts(limit: $limit, offset: $offset, tag: $tag) {
      id
      title
      slug
      excerpt
      coverImage
      author {
        id
        username
        avatar
      }
      tags
      commentCount
      viewCount
      publishedAt
    }
  }
`;

// 获取单个文章
export const POST_QUERY = gql`
  query Post($id: ID!) {
    post(id: $id) {
      id
      title
      slug
      content
      coverImage
      author {
        id
        username
        avatar
        bio
      }
      tags
      comments {
        id
        content
        author {
          id
          username
          avatar
        }
        replies {
          id
          content
          author {
            id
            username
          }
        }
      }
      viewCount
      publishedAt
    }
  }
`;

// 登录 mutation
export const LOGIN_MUTATION = gql`
  mutation Login($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
      user {
        id
        username
        email
      }
    }
  }
`;

// 创建文章 mutation
export const CREATE_POST_MUTATION = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      slug
    }
  }
`;
jsx
// components/PostList.jsx
import { useQuery } from "@apollo/client";
import { POSTS_QUERY } from "../hooks/useGraphQL";

export default function PostList({ tag }) {
  const { data, loading, error, fetchMore } = useQuery(POSTS_QUERY, {
    variables: { limit: 10, offset: 0, tag },
    notifyOnNetworkStatusChange: true,
  });

  if (loading && !data) return <div>加载中...</div>;
  if (error) return <div>加载失败: {error.message}</div>;

  return (
    <div className="post-list">
      {data.posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}

      {data.posts.length >= 10 && (
        <button
          onClick={() => {
            fetchMore({
              variables: {
                offset: data.posts.length,
              },
            });
          }}
        >
          加载更多
        </button>
      )}
    </div>
  );
}

数据加载优化

DataLoader

javascript
// lib/graphql/dataloader.js
import DataLoader from "dataloader";
import User from "../models/User.js";
import Post from "../models/Post.js";

// 创建 DataLoader 实例
export function createLoaders() {
  return {
    userLoader: new DataLoader(async (ids) => {
      const users = await User.find({ _id: { $in: ids } });
      const userMap = new Map(users.map((u) => [u.id, u]));
      return ids.map((id) => userMap.get(id) || null);
    }),

    postLoader: new DataLoader(async (ids) => {
      const posts = await Post.find({ _id: { $in: ids } });
      const postMap = new Map(posts.map((p) => [p.id, p]));
      return ids.map((id) => postMap.get(id) || null);
    }),

    userByUsername: new DataLoader(async (usernames) => {
      const users = await User.find({ username: { $in: usernames } });
      const userMap = new Map(users.map((u) => [u.username, u]));
      return usernames.map((u) => userMap.get(u) || null);
    }),
  };
}

GraphQL 优势

精确获取需要的数据、减少网络请求、强大的类型系统、良好的开发体验。

总结

使用 Claude Code 搭建 GraphQL API:

  • 使用 Apollo Server 快速搭建
  • 设计清晰的 TypeDefs 类型定义
  • 实现高效的 Resolvers
  • 使用 DataLoader 优化 N+1 问题
  • 配置 Apollo Client 进行前端集成
  • 实现错误处理和认证授权