Verified by Garnet Grid

GraphQL API Design & Best Practices

Design production GraphQL APIs. Covers schema design, resolver patterns, N+1 problem, pagination, authentication, rate limiting, and GraphQL vs REST decision framework.

GraphQL lets clients request exactly the data they need — no over-fetching, no under-fetching. But the flexibility that makes GraphQL powerful also makes it dangerous: a single query can trigger thousands of database calls, exhaust server resources, or expose data the client shouldn’t see. This guide covers how to build GraphQL APIs that are fast, secure, and maintainable.


Schema Design

type Query {
  # Single resource by ID
  order(id: ID!): Order
  
  # Paginated list with filters
  orders(
    first: Int = 20
    after: String
    filter: OrderFilter
  ): OrderConnection!
}

type OrderConnection {
  edges: [OrderEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type OrderEdge {
  node: Order!
  cursor: String!
}

type Order {
  id: ID!
  status: OrderStatus!
  total: Money!
  customer: Customer!     # Resolved lazily
  items: [OrderItem!]!    # Resolved lazily
  createdAt: DateTime!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}

input OrderFilter {
  status: OrderStatus
  createdAfter: DateTime
  customerId: ID
}

N+1 Problem → DataLoader

# WITHOUT DataLoader: N+1 queries
# Query: { orders { customer { name } } }
# 1 query: SELECT * FROM orders
# N queries: SELECT * FROM customers WHERE id = ? (once per order)

# WITH DataLoader: 2 queries total
from graphql import DataLoader

class CustomerLoader(DataLoader):
    async def batch_load(self, customer_ids):
        # Single query: SELECT * FROM customers WHERE id IN (...)
        customers = await db.fetch_all(
            "SELECT * FROM customers WHERE id = ANY($1)",
            customer_ids
        )
        customer_map = {c.id: c for c in customers}
        return [customer_map.get(id) for id in customer_ids]

# Resolver
def resolve_customer(order, info):
    return info.context.customer_loader.load(order.customer_id)

Security

Query Complexity Limiting

# Prevent expensive queries
COMPLEXITY_LIMIT = 1000

# Each field has a cost:
# - Scalar field: 1
# - Object field: 5
# - List field: 10 * estimated items

# This query would be rejected:
# query {
#   orders(first: 100) {     # 10 * 100 = 1000
#     items {                  # 10 * 5 = 50 per order = 5000
#       product {              # 5 per item
#         reviews {            # 10 * 10 = 100 per product
#         }
#       }
#     }
#   }
# }
# Total: ~6000 > 1000 limit → REJECTED

GraphQL vs REST Decision

FactorChoose RESTChoose GraphQL
Client diversityFew clients, similar needsMany clients, different data needs
Data relationshipsFlat resourcesDeeply nested, interconnected data
CachingHTTP caching criticalClient-side caching acceptable
Team experienceREST experienceGraphQL experience or willingness to learn
API consumersExternal / public APIInternal / mobile apps

Anti-Patterns

Anti-PatternProblemFix
No query depth limitInfinitely nested queries crash serverSet max depth (typically 5-10)
No DataLoaderN+1 database queries per requestDataLoader for all relationship resolvers
Exposing database schemaSchema changes break clientsSeparate GraphQL schema from DB schema
No paginationSingle query returns millions of rowsCursor-based pagination (Relay spec)
Mutations returning voidClient must re-fetch after mutationReturn updated object from mutations

Checklist

  • Schema design: types, connections, input types, enums
  • DataLoader for all relationship resolvers (N+1 prevention)
  • Pagination: cursor-based for all list queries
  • Query complexity analysis with limits
  • Query depth limiting (max 5-10 levels)
  • Authentication and field-level authorization
  • Error handling: structured errors with codes
  • Rate limiting: per-query or per-complexity-unit

:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For API design consulting, visit garnetgrid.com. :::

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →