Skip to main content
โšก Calmops

GraphQL API Design: Schema, Queries, and Best Practices

Introduction

GraphQL provides a flexible alternative to REST, allowing clients to request exactly the data they need. This guide covers GraphQL schema design, query patterns, and best practices.

Schema Design

# Schema definition
type Query {
  user(id: ID!): User
  users(first: Int = 10, after: String): UserConnection!
  post(id: ID!): Post
  posts(author: ID, category: String, first: Int = 10): PostConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
  publishPost(id: ID!): Post!
}

type Subscription {
  userCreated: User!
  postPublished(authorId: ID): Post!
}

# Types
type User {
  id: ID!
  email: String!
  name: String!
  posts(first: Int = 10): PostConnection!
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  category: String!
  publishedAt: String
  comments(first: Int = 10): CommentConnection!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  createdAt: String!
}

# Connections for 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
}

input CreateUserInput {
  email: String!
  name: String!
}

input UpdateUserInput {
  name: String
  email: String
}

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
  category: String!
}

Resolver Implementation

import strawberry
from typing import List, Optional

@strawberry.type
class User:
    id: str
    email: str
    name: str
    created_at: str
    
    @strawberry.field
    def posts(self, first: int = 10) -> "PostConnection":
        return PostService.get_posts_by_author(self.id, first)

@strawberry.type
class Query:
    @strawberry.field
    def user(self, id: str) -> Optional[User]:
        return UserService.get_by_id(id)
    
    @strawberry.field
    def users(self, first: int = 10, after: str = None) -> UserConnection:
        return UserService.get_users(first, after)

@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_user(self, input: CreateUserInput) -> User:
        return UserService.create(input.email, input.name)
    
    @strawberry.mutation
    def delete_user(self, id: str) -> bool:
        return UserService.delete(id)

# Subscription
@strawberry.type
class Subscription:
    @strawberry.subscription
    def user_created(self) -> User:
        pubsub = get_pubsub()
        for user in pubsub.subscribe("USER_CREATED"):
            yield user

schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)

Query Patterns

# Fetch user with posts
query GetUserWithPosts($userId: ID!) {
  user(id: $userId) {
    id
    name
    email
    posts(first: 5) {
      edges {
        node {
          id
          title
          publishedAt
        }
      }
    }
  }
}

# Batch query
query Dashboard($userId: ID!) {
  user(id: $userId) {
    ...UserFields
  }
  recentPosts: posts(first: 5) {
    ...PostFields
  }
  notifications(unread: true) {
    id
    message
  }
}

fragment UserFields on User {
  id
  name
  posts(first: 3) {
    totalCount
  }
}

fragment PostFields on PostConnection {
  edges {
    node {
      id
      title
    }
  }
}

Conclusion

GraphQL provides flexibility for clients while simplifying backend APIs. Design schemas around client needs, use connections for lists, implement proper error handling, and optimize with DataLoader for N+1 prevention.

Resources

  • GraphQL.org
  • Apollo Documentation

Comments