All skills
apollographql avatar

/graphql-schema

@79d2b07 official
by Apollo GraphQLapollographql/skills115 stars
13

Guide for designing and changing GraphQL schemas following industry best practices. Use this skill whenever you create or edit GraphQL SDL, such as a `schema.graphql` file or `typeDefs`, including when: (1) designing a new GraphQL schema or API, (2) adding or changing types, fields, arguments, mutations, or descriptions in an existing schema, (3) reviewing existing schema for improvements, (4) deciding on type structures or nullability, (5) implementing pagination or error patterns, (6) ensuring security in schema design.

Use this Skill: https://skilld.dev/gh/apollographql/skills/graphql-schema

This session only. Nothing lands on disk.

referencespagination.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Pagination Design

This reference covers pagination patterns for GraphQL schemas, with focus on the cursor-based Connection pattern.

Table of Contents

Pagination Approaches

Simple List (No Pagination)

Only use for small, bounded collections:

type User {
  # OK: Users typically have few roles
  roles: [Role!]!

  # OK: Limited enum values
  permissions: [Permission!]!
}

Offset-Based

Straightforward approach with limitations:

type Query {
  posts(offset: Int = 0, limit: Int = 20): [Post!]!
}

Cursor-Based (Connection Pattern)

Recommended for most cases:

type Query {
  posts(first: Int, after: String): PostConnection!
}

Offset vs Cursor

Offset-Based Pagination

type Query {
  posts(offset: Int = 0, limit: Int = 20): PostsPage!
}

type PostsPage {
  items: [Post!]!
  totalCount: Int!
  hasMore: Boolean!
}

Pros:

  • Straightforward to build
  • Allows jumping to specific page
  • Familiar to REST developers

Cons:

  • Inconsistent with real-time data (items shift)
  • Poor performance on large offsets
  • Duplicate or missing items when data changes

Cursor-Based Pagination

type Query {
  posts(first: Int, after: String): PostConnection!
}

Pros:

  • Stable pagination (cursor points to specific item)
  • Efficient for large datasets
  • Works well with real-time updates
  • Industry standard (Relay specification)

Cons:

  • Can't jump to arbitrary page
  • Requires more code to build
  • Opaque cursors require explanation

Connection Pattern

Relay Connection Specification

The Connection pattern is defined by the Relay specification and widely adopted:

type Query {
  posts(
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Connection Arguments

Argument Purpose
first Number of items from the start
after Cursor to start after (forward pagination)
last Number of items from the end
before Cursor to start before (backward pagination)

Usage patterns:

  • Forward: first + after
  • Backward: last + before
  • Don't mix forward and backward in same request

Edge Type

Edges contain:

  • node: The actual item
  • cursor: Opaque cursor for this item
  • Additional edge-specific data (optional)
type PostEdge {
  node: Post!
  cursor: String!
  # Edge-specific metadata
  addedAt: DateTime
  addedBy: User
}

PageInfo Type

type PageInfo {
  hasNextPage: Boolean!      # More items forward?
  hasPreviousPage: Boolean!  # More items backward?
  startCursor: String        # Cursor of first item
  endCursor: String          # Cursor of last item
}

Building Connections

Basic Connection Query

type Query {
  # Simple connection
  posts(first: Int, after: String): PostConnection!

  # Connection with filters
  userPosts(
    userId: ID!
    first: Int
    after: String
    status: PostStatus
  ): PostConnection!
}

Connection for Relationships

type User {
  id: ID!
  name: String!

  # Paginated relationship
  posts(first: Int, after: String): PostConnection!
  followers(first: Int, after: String): UserConnection!
  following(first: Int, after: String): UserConnection!
}

Cursor Design

Cursors should be:

  • Opaque: Clients shouldn't parse them
  • Stable: Same cursor = same position
  • Serializable: Usually base64-encoded
// Common cursor strategies:

// 1. Encoded ID (simple)
const cursor = base64(`id:${post.id}`);

// 2. Encoded timestamp + ID (for sorted lists)
const cursor = base64(`${post.createdAt}:${post.id}`);

// 3. Encoded offset (simpler, but less stable)
const cursor = base64(`offset:${index}`);

Default Page Size

Always set sensible defaults and limits:

type Query {
  posts(
    first: Int = 20  # Default page size
    after: String
  ): PostConnection!
}

In resolver, enforce maximum:

const resolvers = {
  Query: {
    posts: (_, { first = 20, after }) => {
      const limit = Math.min(first, 100); // Cap at 100
      // ...
    }
  }
};

Sorting and Filtering

Sorting Arguments

enum PostOrderField {
  CREATED_AT
  UPDATED_AT
  TITLE
  POPULARITY
}

input PostOrder {
  field: PostOrderField!
  direction: OrderDirection!
}

enum OrderDirection {
  ASC
  DESC
}

type Query {
  posts(
    first: Int
    after: String
    orderBy: PostOrder = { field: CREATED_AT, direction: DESC }
  ): PostConnection!
}

Filtering Arguments

input PostFilter {
  status: PostStatus
  authorId: ID
  createdAfter: DateTime
  createdBefore: DateTime
  tags: [String!]
}

type Query {
  posts(
    first: Int
    after: String
    filter: PostFilter
    orderBy: PostOrder
  ): PostConnection!
}

Combined Example

type Query {
  posts(
    first: Int = 20
    after: String
    last: Int
    before: String
    filter: PostFilter
    orderBy: PostOrder
  ): PostConnection!
}

# Usage:
# query {
#   posts(
#     first: 10
#     filter: { status: PUBLISHED, tags: ["graphql"] }
#     orderBy: { field: CREATED_AT, direction: DESC }
#   ) {
#     edges {
#       node { id title }
#       cursor
#     }
#     pageInfo {
#       hasNextPage
#       endCursor
#     }
#   }
# }

Performance Considerations

Avoid COUNT(*) for totalCount

totalCount can be expensive. Options:

  1. Make it nullable and skip when expensive
  2. Return an estimate
  3. Use a separate query for count
type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int  # Nullable - may not always be computed
}

Use Efficient Cursors

Cursor-based pagination should use indexed columns:

-- Efficient: Uses index on created_at
SELECT * FROM posts
WHERE created_at < $cursor_timestamp
ORDER BY created_at DESC
LIMIT 20;

-- Inefficient: Full table scan
SELECT * FROM posts
LIMIT 20 OFFSET 10000;

Limit Maximum Page Size

Prevent clients from requesting too many items:

const MAX_PAGE_SIZE = 100;

function resolveConnection(first: number | null) {
  const limit = Math.min(first ?? 20, MAX_PAGE_SIZE);
  // ...
}

Index Cursor Columns

Ensure database indexes exist for cursor columns:

CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at DESC);

Consider Denormalization

For very large datasets, consider:

  • Materialized views
  • Denormalized count columns
  • Cached aggregations

Source: SKILL.md on GitHub

No alerts1d5 checks · Risk SAFE
  • Gen Agent Trust Hub1d

    The skill is a comprehensive guide for GraphQL schema design, providing best practices for types, naming, pagination, error handling, and security. It is purely documentation and contains no executable code or malicious instructions.

  • Socket1d

    No alerts

  • Snyk1d

    Risk: LOW · No issues

  • Runlayer7mo

    1/6 files flagged

  • ZeroLeaks5mo

    1 finding · Score: 86/100

Signed by skilld at 79d2b07. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 2 days ago
What it can do
Runs commands
compatibility
Any GraphQL implementation (Apollo Server, graphql-js, Yoga, etc.)
metadata
{
  "author": "apollographql",
  "version": "1.0.2"
}
All 1 allowed tools
Bash(npm:*) Bash(npx:*) Read Write Edit Glob Grep
  • Security
  • graphql
  • schema-design
  • api-design
  • apollo
  • nullability
  • pagination
  • mutations
  • type-design
  • backwards-compatibility

README badge

README badge for apollographql/skills/graphql-schema

Instructional skill covering GraphQL schema design best practices, including type definition patterns, nullability rules, pagination, error handling, and security considerations. Applicable to any GraphQL implementation (Apollo Server, graphql-js, Yoga) and targets decisions around type structures, backwards compatibility, and API usability.

Generated from the current SKILL.md.

Does this skill work with any GraphQL server library?
Yes. The skill is compatible with any GraphQL implementation including Apollo Server, graphql-js, Yoga, and others. It provides design guidance independent of server framework.
What topics does this skill cover?
The skill covers type design, nullability rules, input vs output types, interfaces, unions, pagination patterns, error modeling, security best practices, naming conventions, and mutation design.
Is this for existing schema review or new schema design?
Both. The skill applies when designing a new GraphQL schema, reviewing and improving existing schemas, deciding on type structures, or implementing specific patterns like pagination or error handling.
Does this include code generation or validation tools?
No. This skill is a design guide with reference documentation and best practices. It does not generate or validate schemas automatically.

Generated from the current SKILL.md. These answers refresh after source changes.