- Published on
GraphQL Best Practices: The Definitive Developer Guide
- Authors

- Name
- Andrew Blase
GraphQL gives you a superpower: ask for exactly what you need. But that superpower becomes a liability when your graph is designed without discipline. This guide covers everything you need to build GraphQL APIs that are fast, maintainable, and ready to scale.
What is GraphQL?
GraphQL is a query language for APIs and a runtime for executing those queries against your data. Unlike REST, where the server decides what data to return, GraphQL lets the client specify exactly what it needs — no more, no less.
Three things make GraphQL fundamentally different from REST:
- Single endpoint — all queries go to one URL
- Typed schema — every field has a type, enforced at runtime
- Client-driven queries — clients request the shape of data they need
For a deeper look at how graph theory underpins GraphQL's design, read why REST-QL APIs fail and how to avoid building one.
GraphQL Schema Design Best Practices
Your schema is your API contract. Get it wrong and you'll be paying the price for years.
Design for the Client, Not the Database
Your schema should reflect how clients consume data, not how your database stores it. A common mistake is building a GraphQL schema that maps 1:1 to database tables — this creates a REST-QL API that happens to use GraphQL syntax.
Instead, think in terms of user journeys. What does the client need to display a product page? Model that directly.
Use the Code-First Approach
The code-first approach lets you define your schema in code using a type system, then generate the SDL automatically. This eliminates the dual maintenance problem of keeping schema files and resolvers in sync.
Tools like Nexus GraphQL with Prisma ORM make this practical at scale. See how to implement code-first GraphQL with Nexus and Prisma for a full walkthrough.
Name Types and Fields Consistently
- Use
PascalCasefor type names:UserProfile, notuser_profile - Use
camelCasefor field names:firstName, notfirst_name - Be explicit over abbreviated:
createdAt, notcreated - Use descriptive names that reflect the business domain, not implementation details
Design for Nullability Intentionally
Every field in GraphQL can be nullable by default. This is a trap. Nullable fields push error handling to the client and make queries unpredictable.
Be explicit about what can and cannot be null. Non-null fields (String!) are a contract that the field will always be present. Break that contract and you break clients.
Implement the Relay Spec for Pagination
The Relay cursor-based pagination spec is the standard for GraphQL pagination:
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo!
}
type UserEdge {
node: User
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
This gives clients everything they need to implement infinite scroll or traditional pagination without additional API design decisions.
GraphQL vs REST: When to Use Each
| Scenario | GraphQL | REST |
|---|---|---|
| Multiple clients with different data needs | Better | Worse |
| Simple CRUD with predictable data shapes | Equal | Better |
| Real-time subscriptions | Better | Worse |
| Third-party public API | Worse | Better |
| Microservices federation | Better | Worse |
| Team new to APIs | Worse (learning curve) | Better |
GraphQL shines when you have multiple clients (web, mobile, third-party) consuming the same API with different data needs. REST wins for simple use cases where the overhead isn't justified.
GraphQL Federation: Scaling Across Teams
As your organization grows, a single monolithic GraphQL schema becomes a bottleneck. Federation solves this by letting each team own their slice of the graph.
What is Federation?
Federation is an architecture where multiple GraphQL services (subgraphs) compose into a single unified API (supergraph). Each team owns and deploys their subgraph independently. A router sits in front, routing queries to the right subgraph.
Federation Best Practices
Define clear ownership boundaries. Each type should be owned by one subgraph. Avoid splitting the same type across multiple subgraphs unless you're using entity extensions intentionally.
Use a schema registry. Without a central registry, schema changes become uncoordinated and breaking changes slip through. Tools like WunderGraph Cosmo's schema registry enforce composition checks before deployment.
Start with governance. Federation without governance produces chaos. Read the guide to GraphQL governance before spinning up your first subgraph.
Choose the right router. The router is the critical path for every query. The WunderGraph Cosmo Router benchmarks at 10x faster than Apollo Router and is fully open source.
GraphQL Performance Best Practices
Solve the N+1 Problem with DataLoader
The N+1 problem is the most common GraphQL performance issue. It happens when resolving a list of items triggers N additional queries to fetch related data.
// Bad: N+1 queries
const resolvers = {
Query: {
users: () => db.users.findMany(),
},
User: {
posts: (user) => db.posts.findMany({ where: { userId: user.id } }),
},
}
// Good: DataLoader batches queries
const resolvers = {
User: {
posts: (user, _, { loaders }) => loaders.postsByUserId.load(user.id),
},
}
DataLoader batches all posts queries from a single request into one database call.
Use Query Complexity Limits
GraphQL's flexibility means clients can write arbitrarily complex queries. Without limits, a single malicious or poorly written query can bring down your server.
Set a maximum query complexity score and reject queries that exceed it:
const server = new ApolloServer({
plugins: [
createComplexityPlugin({
schema,
maximumComplexity: 1000,
onComplete: (complexity) => {
console.log(`Query complexity: ${complexity}`)
},
}),
],
})
Implement Persisted Queries
Persisted queries cache query documents on the server by hash. This reduces payload size, enables better caching, and prevents arbitrary query execution in production.
Enable Response Caching
GraphQL responses can be cached at the field level using cache hints:
type Product @cacheControl(maxAge: 3600) {
id: ID!
name: String!
price: Float!
}
GraphQL Security Best Practices
Authenticate and Authorize at the Resolver Level
Do not rely on network-level security alone. Every resolver that returns sensitive data should check whether the requesting user is authorized.
const resolvers = {
Query: {
adminDashboard: (_, __, { user }) => {
if (!user || user.role !== 'ADMIN') {
throw new ForbiddenError('Not authorized')
}
return getDashboardData()
},
},
}
Limit Query Depth
Deep queries can cause recursive performance issues. Limit the maximum depth of queries your API accepts:
const depthLimit = require('graphql-depth-limit')
const server = new ApolloServer({
validationRules: [depthLimit(10)],
})
Disable Introspection in Production
Introspection lets anyone map your entire schema. Disable it in production to reduce your attack surface:
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production',
})
For a deep dive into securing a federated graph, see the WunderGraph Cosmo security guide.
GraphQL Governance: Keeping Your Graph Clean
Governance is what separates a GraphQL API that scales from one that becomes technical debt.
Schema reviews. Treat schema changes like code reviews. Every new type, field, or mutation should go through a review process before it lands in production.
Deprecation over deletion. Never remove a field without deprecating it first. Clients may be using it in ways you don't know about.
Consistent naming conventions. Enforce naming conventions across your entire schema. Inconsistency compounds as the schema grows.
Analytics and observability. Know which fields are actually being queried. Unused fields are candidates for deprecation. For this, tools like WunderGraph Cosmo's analytics give you field-level usage data.
For a comprehensive guide to governance at scale, see Mastering GraphQL Complexity: A Guide to Effective Governance.
Real-Time with GraphQL Subscriptions
GraphQL subscriptions enable real-time data delivery over WebSockets. They follow the same query language as queries and mutations, making them natural to implement.
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
content
author {
name
}
}
}
In federated architectures, subscriptions require special handling. WunderGraph Cosmo's Event-Driven Federated Subscriptions (EDFS) solve the hardest problems: stateless subgraphs, single WebSocket connections per client, and scalable event delivery.
GraphQL and AI: The Emerging Frontier
Large language models work best with structured, predictable data sources. GraphQL's typed schema and precise querying make it an ideal data layer for AI agents.
Key reasons GraphQL and AI work well together:
- Token efficiency: AI models have token limits. GraphQL fetches only the fields needed, reducing token consumption.
- Schema as documentation: The type system is self-documenting, making it easier for LLMs to understand available data.
- Flexible querying: AI agents can construct ad-hoc queries without a fixed API contract.
For more on this intersection, see how GraphQL serves as a data source for large language models.
Getting Your Organization to Adopt GraphQL
The technical case for GraphQL is strong. The organizational case is harder. Engineers resist change, managers fear risk, and teams disagree on standards.
The key is to lead with pain points, not features. Find a real problem that GraphQL solves — overfetching, multiple round trips, inconsistent REST endpoints — and build a proof of concept around it. Small wins build momentum.
For a complete playbook on driving GraphQL adoption, including how to handle the four types of stakeholder reactions, read how to get your company to adopt GraphQL.
Summary
GraphQL best practices in one place:
- Schema design — design for the client, use code-first, implement Relay pagination
- Federation — use a schema registry, define ownership, choose your router carefully
- Performance — solve N+1 with DataLoader, set complexity limits, use persisted queries
- Security — authorize at the resolver level, limit depth, disable introspection in production
- Governance — treat schema changes like code, deprecate before deleting, track field usage
- Real-time — use subscriptions for live data, EDFS for federated subscriptions
- AI — GraphQL is an ideal data source for LLMs; lean into the type system
Ready to go deeper? The posts linked throughout this guide cover each topic in detail.