Apollo GraphQL
"Apollo GraphQL: schema design, resolvers, Apollo Server, Apollo Client with React, useQuery/useMutation, caching strategies, subscriptions, and codegen"
GraphQL provides a typed query language where clients request exactly the data they need — no over-fetching, no under-fetching. Apollo is the most widely adopted GraphQL ecosystem, providing Apollo Server for the backend and Apollo Client for the frontend with a sophisticated normalized cache. ## Key Points - Use DataLoader for every relationship resolver to batch database queries and eliminate N+1 problems. - Define `typePolicies` in the InMemoryCache to control how paginated and merged data behaves. - Throw `GraphQLError` with extension codes (`UNAUTHENTICATED`, `FORBIDDEN`, `BAD_USER_INPUT`) for structured error handling. - Run codegen in CI to keep generated types in sync with the schema; never hand-write query types. - Use `cache.modify` and `cache.evict` for precise cache updates after mutations rather than refetching entire queries. - Keep resolvers thin — delegate business logic to service functions that are independently testable. - **N+1 queries without DataLoader.** Field resolvers that query the database individually per parent record destroy performance at scale. - **Fetching everything with `SELECT *` style queries.** GraphQL's value is selective field fetching; design resolvers to respect the requested field set. - **Disabling the cache.** Apollo Client's normalized cache is its primary advantage; work with `typePolicies` instead of setting `fetchPolicy: "no-cache"` everywhere. - **Deeply nested schemas without limits.** Unbounded nesting (user -> posts -> comments -> author -> posts) allows malicious queries; use depth limiting. - **Putting auth logic in every resolver.** Use schema directives or middleware-style plugins for cross-cutting authentication and authorization. - **Returning raw database models.** Map database entities to GraphQL types explicitly to avoid leaking internal fields or sensitive data.
skilldb get api-frameworks-skills/Apollo GraphQLFull skill: 424 linesApollo GraphQL
Core Philosophy
GraphQL provides a typed query language where clients request exactly the data they need — no over-fetching, no under-fetching. Apollo is the most widely adopted GraphQL ecosystem, providing Apollo Server for the backend and Apollo Client for the frontend with a sophisticated normalized cache.
The schema serves as the contract between client and server. Resolvers implement the data-fetching logic for each field. On the client, Apollo Client manages server state with automatic caching, refetching, and optimistic updates through React hooks. Combined with codegen, every query, mutation, and subscription is fully type-safe from schema to component.
Setup
Apollo Server
// Install: npm install @apollo/server graphql
// src/schema.ts — Type definitions
export const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
comments: [Comment!]!
createdAt: String!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
type Query {
users: [User!]!
user(id: ID!): User
posts(published: Boolean, limit: Int, offset: Int): [Post!]!
post(id: ID!): Post
}
input CreatePostInput {
title: String!
content: String!
published: Boolean = false
}
input UpdatePostInput {
title: String
content: String
published: Boolean
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
addComment(postId: ID!, text: String!): Comment!
}
type Subscription {
commentAdded(postId: ID!): Comment!
postPublished: Post!
}
`;
// src/server.ts
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { createServer } from "http";
import express from "express";
import cors from "cors";
import { typeDefs } from "./schema";
import { resolvers } from "./resolvers";
interface Context {
user: { id: string; role: string } | null;
db: typeof prisma;
}
const app = express();
const httpServer = createServer(app);
const server = new ApolloServer<Context>({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
app.use(
"/graphql",
cors({ origin: "http://localhost:3000" }),
express.json(),
expressMiddleware(server, {
context: async ({ req }): Promise<Context> => {
const token = req.headers.authorization?.replace("Bearer ", "");
const user = token ? await verifyToken(token) : null;
return { user, db: prisma };
},
})
);
httpServer.listen(4000);
Key Techniques
Resolvers
// src/resolvers.ts
import { GraphQLError } from "graphql";
export const resolvers = {
Query: {
users: async (_: unknown, __: unknown, ctx: Context) => {
return ctx.db.user.findMany();
},
user: async (_: unknown, args: { id: string }, ctx: Context) => {
return ctx.db.user.findUnique({ where: { id: args.id } });
},
posts: async (
_: unknown,
args: { published?: boolean; limit?: number; offset?: number },
ctx: Context
) => {
return ctx.db.post.findMany({
where: args.published !== undefined ? { published: args.published } : undefined,
take: args.limit ?? 20,
skip: args.offset ?? 0,
orderBy: { createdAt: "desc" },
});
},
},
Mutation: {
createPost: async (_: unknown, args: { input: CreatePostInput }, ctx: Context) => {
if (!ctx.user) {
throw new GraphQLError("Authentication required", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return ctx.db.post.create({
data: { ...args.input, authorId: ctx.user.id },
});
},
deletePost: async (_: unknown, args: { id: string }, ctx: Context) => {
if (!ctx.user) {
throw new GraphQLError("Authentication required", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const post = await ctx.db.post.findUnique({ where: { id: args.id } });
if (post?.authorId !== ctx.user.id && ctx.user.role !== "admin") {
throw new GraphQLError("Not authorized", {
extensions: { code: "FORBIDDEN" },
});
}
await ctx.db.post.delete({ where: { id: args.id } });
return true;
},
},
// Field-level resolvers for relationships
User: {
posts: async (parent: { id: string }, _: unknown, ctx: Context) => {
return ctx.db.post.findMany({ where: { authorId: parent.id } });
},
},
Post: {
author: async (parent: { authorId: string }, _: unknown, ctx: Context) => {
return ctx.db.user.findUnique({ where: { id: parent.authorId } });
},
comments: async (parent: { id: string }, _: unknown, ctx: Context) => {
return ctx.db.comment.findMany({ where: { postId: parent.id } });
},
},
};
DataLoader for N+1 Prevention
// src/loaders.ts
import DataLoader from "dataloader";
export function createLoaders(db: typeof prisma) {
return {
userLoader: new DataLoader<string, User>(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: [...ids] } } });
const userMap = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => userMap.get(id)!);
}),
postsByAuthorLoader: new DataLoader<string, Post[]>(async (authorIds) => {
const posts = await db.post.findMany({
where: { authorId: { in: [...authorIds] } },
});
const grouped = new Map<string, Post[]>();
posts.forEach((p) => {
const list = grouped.get(p.authorId) ?? [];
list.push(p);
grouped.set(p.authorId, list);
});
return authorIds.map((id) => grouped.get(id) ?? []);
}),
};
}
// Use in resolvers
Post: {
author: (parent, _, ctx) => ctx.loaders.userLoader.load(parent.authorId),
},
User: {
posts: (parent, _, ctx) => ctx.loaders.postsByAuthorLoader.load(parent.id),
},
Apollo Client Setup
// Install: npm install @apollo/client graphql
// src/lib/apollo.ts
import { ApolloClient, InMemoryCache, createHttpLink, split } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
import { createClient } from "graphql-ws";
const httpLink = createHttpLink({ uri: "/graphql" });
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("token");
return {
headers: { ...headers, authorization: token ? `Bearer ${token}` : "" },
};
});
const wsLink = new GraphQLWsLink(
createClient({ url: "ws://localhost:4000/graphql" })
);
const splitLink = split(
({ query }) => {
const def = getMainDefinition(query);
return def.kind === "OperationDefinition" && def.operation === "subscription";
},
wsLink,
authLink.concat(httpLink)
);
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
keyArgs: ["published"],
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
});
React Hooks — useQuery and useMutation
// src/components/PostList.tsx
import { gql, useQuery, useMutation } from "@apollo/client";
const GET_POSTS = gql`
query GetPosts($published: Boolean, $limit: Int, $offset: Int) {
posts(published: $published, limit: $limit, offset: $offset) {
id
title
content
published
author {
id
name
}
createdAt
}
}
`;
const DELETE_POST = gql`
mutation DeletePost($id: ID!) {
deletePost(id: $id)
}
`;
export function PostList() {
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
variables: { published: true, limit: 10, offset: 0 },
});
const [deletePost] = useMutation(DELETE_POST, {
update(cache, _, { variables }) {
cache.modify({
fields: {
posts(existing = [], { readField }) {
return existing.filter(
(ref: any) => readField("id", ref) !== variables?.id
);
},
},
});
},
optimisticResponse: { deletePost: true },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<button onClick={() => deletePost({ variables: { id: post.id } })}>
Delete
</button>
</article>
))}
<button onClick={() =>
fetchMore({ variables: { offset: data.posts.length } })
}>
Load more
</button>
</div>
);
}
Code Generation for Type Safety
// Install: npm install -D @graphql-codegen/cli @graphql-codegen/typescript
// @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql",
documents: ["src/**/*.tsx", "src/**/*.ts"],
generates: {
"src/generated/graphql.ts": {
plugins: [
"typescript",
"typescript-operations",
"typescript-react-apollo",
],
config: {
withHooks: true,
withHOC: false,
withComponent: false,
},
},
},
};
export default config;
// After running: npx graphql-codegen
// Usage with generated hooks:
import { useGetPostsQuery, useDeletePostMutation } from "@/generated/graphql";
function Posts() {
const { data, loading } = useGetPostsQuery({ variables: { published: true } });
const [deletePost] = useDeletePostMutation();
// Fully typed data.posts, variables, etc.
}
Best Practices
- Use DataLoader for every relationship resolver to batch database queries and eliminate N+1 problems.
- Define
typePoliciesin the InMemoryCache to control how paginated and merged data behaves. - Throw
GraphQLErrorwith extension codes (UNAUTHENTICATED,FORBIDDEN,BAD_USER_INPUT) for structured error handling. - Run codegen in CI to keep generated types in sync with the schema; never hand-write query types.
- Use
cache.modifyandcache.evictfor precise cache updates after mutations rather than refetching entire queries. - Keep resolvers thin — delegate business logic to service functions that are independently testable.
Anti-Patterns
- N+1 queries without DataLoader. Field resolvers that query the database individually per parent record destroy performance at scale.
- Fetching everything with
SELECT *style queries. GraphQL's value is selective field fetching; design resolvers to respect the requested field set. - Disabling the cache. Apollo Client's normalized cache is its primary advantage; work with
typePoliciesinstead of settingfetchPolicy: "no-cache"everywhere. - Deeply nested schemas without limits. Unbounded nesting (user -> posts -> comments -> author -> posts) allows malicious queries; use depth limiting.
- Putting auth logic in every resolver. Use schema directives or middleware-style plugins for cross-cutting authentication and authorization.
- Returning raw database models. Map database entities to GraphQL types explicitly to avoid leaking internal fields or sensitive data.
Install this skill directly: skilldb add api-frameworks-skills
Related Skills
Elysia
"Elysia: Bun-native web framework with type-safe routing, TypeBox validation, plugins, Eden treaty client, lifecycle hooks, and Swagger documentation"
Express.js
"Express.js with TypeScript: routing, middleware patterns, error handling, validation, authentication, static files, CORS, and production-ready configuration"
Fastify
Fastify: high-performance Node.js web framework with schema-based validation, logging, plugin architecture, and TypeScript support
Hono
"Hono: ultra-fast web framework for edge, serverless, and Node.js — middleware, routing, Zod validation, JWT, CORS, RPC client, and JSX support"
Koa
Koa: lightweight Node.js framework by the Express team with async/await middleware, context object, and composable architecture
NestJS
NestJS: progressive Node.js framework with decorators, dependency injection, modules, guards, pipes, and interceptors for scalable APIs