$3f
Patterns canoniques pour utiliser Apollo GraphQL côté serveur et client en 2026.
npm install @apollo/server graphql @apollo/datasource-rest dataloader
// server.ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { typeDefs } from './schema.js'
import { resolvers } from './resolvers.js'
const server = new ApolloServer({
typeDefs,
resolvers,
csrfPrevention: true,
introspection: process.env.NODE_ENV !== 'production',
formatError: (formattedError, error) => {
// Strip stack traces in production
if (process.env.NODE_ENV === 'production') {
return { message: formattedError.message, code: formattedError.extensions?.code }
}
return formattedError
},
})
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
user: await authenticateUser(req.headers.authorization),
loaders: createDataLoaders(),
}),
listen: { port: 4000 },
})
console.log(`🚀 Server ready at ${url}`)
# schema.graphql
type Query {
user(id: ID!): User
users(first: Int = 10, after: String): UserConnection!
search(query: String!, type: SearchType): SearchResult!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(input: UpdateUserInput!): UpdateUserPayload!
}
type Subscription {
userOnline(roomId: ID!): User!
}
# Relay-style cursor pagination
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Errors as data (union)
union CreateUserPayload = CreateUserSuccess | UserError
type CreateUserSuccess {
user: User!
}
type UserError {
message: String!
field: String
code: ErrorCode!
}
enum ErrorCode {
INVALID_INPUT
DUPLICATE
NOT_FOUND
UNAUTHORIZED
}
import DataLoader from 'dataloader'
export function createDataLoaders() {
return {
userById: new DataLoader<string, User>(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: [...ids] } } })
const map = new Map(users.map(u => [u.id, u]))
return ids.map(id => map.get(id) || new Error(`User ${id} not found`))
}),
}
}
// In resolver
const resolvers = {
Post: {
author: (post, _, { loaders }) => loaders.userById.load(post.authorId),
},
}
Sans DataLoader, requêter 100 posts → 100 SELECT auteur. Avec DataLoader → 1 SELECT batched.
import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client'
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
// Pagination merge
keyArgs: false,
merge(existing, incoming) {
return {
...incoming,
edges: [...(existing?.edges ?? []), ...incoming.edges],
}
},
},
},
},
},
}),
headers: {
authorization: `Bearer ${token}`,
},
})
function App() {
return (
<ApolloProvider client={client}>
<UsersList />
</ApolloProvider>
)
}
const GET_USERS = gql`
query GetUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node { id name email }
}
pageInfo { hasNextPage endCursor }
}
}
`
function UsersList() {
const { data, loading, fetchMore } = useQuery(GET_USERS, { variables: { first: 20 } })
// ...
}
# Subgraph: users
extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
# Subgraph: posts
extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
author: User!
}
type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
# Compose supergraph
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# Run Apollo Router (Rust)
./router --supergraph supergraph.graphql
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled'
import depthLimit from 'graphql-depth-limit'
import { createComplexityRule } from 'graphql-validation-complexity'
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7), // max query depth
createComplexityRule({ maximumComplexity: 1000 }),
],
plugins: [
process.env.NODE_ENV === 'production'
? ApolloServerPluginLandingPageDisabled()
: ApolloServerPluginLandingPageGraphQLPlayground(),
],
csrfPrevention: true, // require Apollo-Require-Preflight header
})
Persisted queries : ne permettre que les queries pré-approuvées en prod (anti-DDoS).
# Generate persisted query manifest
npx generate-persisted-query-manifest
# codegen.yml