apollo-graphql-best-practices
1
总安装量
1
周安装量
#43017
全站排名
安装命令
npx skills add https://github.com/maximepzv/apollo-graphql-best-practices --skill apollo-graphql-best-practices
Agent 安装分布
opencode
1
Skill 文档
Apollo GraphQL Best Practices
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
Use errorPolicy and CombinedGraphQLErrors for granular error control:
import { useQuery } from "@apollo/client";
import { CombinedGraphQLErrors } from "@apollo/client/errors";
function Component() {
const { data, error } = useQuery(QUERY, {
errorPolicy: "all" // Receive partial data with errors
});
if (error) {
if (CombinedGraphQLErrors.is(error)) {
// GraphQL errors (validation, resolver errors)
return <div>Error: {error.errors[0].message}</div>;
}
// Network errors
return <div>Network error: {error.message}</div>;
}
return <div>{data?.field}</div>;
}
Cache Type Policies
Configure pagination and field merging:
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Offset-based pagination
items: {
keyArgs: ["filter"], // Cache separately per filter
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;
},
},
},
},
// Entities without id field
Token: {
keyFields: false, // Treat as singleton
},
},
});
Fetch Policies
| Policy | Behavior |
|---|---|
cache-first |
Read cache, fetch if missing (default) |
cache-only |
Only read cache, never fetch |
network-only |
Always fetch, update cache |
no-cache |
Always fetch, don’t cache |
cache-and-network |
Return 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
- Use non-nullable by default – Add
!unless field can legitimately be null - Prefer specific types – Use
ID!for identifiers, custom scalars for dates - Design for the client – Structure schema around UI needs, not database schema
- 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
| Code | Use Case |
|---|---|
UNAUTHENTICATED |
Missing or invalid authentication |
FORBIDDEN |
Authenticated but not authorized |
BAD_USER_INPUT |
Invalid argument values |
NOT_FOUND |
Requested resource doesn’t exist |
INTERNAL_SERVER_ERROR |
Unexpected server errors |
Performance Tips
- Use DataLoader – Batch and cache database calls to avoid N+1 queries
- Implement pagination – Never return unbounded lists
- Use persisted queries – Reduce request size in production
- Enable APM – Use Apollo Studio for query performance monitoring
- Lazy load fragments – Split large queries with
@deferdirective - Configure cache TTL – Set appropriate
maxAgefor cached responses