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 进行前端集成
- 实现错误处理和认证授权