Common GraphQL Schema Patterns for JSON-Based APIs
When wrapping an existing JSON API with a GraphQL layer — or designing a new one from scratch — certain patterns emerge repeatedly. Here are the most useful ones.
Pattern 1: Connection / Pagination
Relay-style connections are the standard way to paginate list fields. Instead of returning a raw array, wrap it with pagination metadata:
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
users(first: Int!, after: String): UserConnection!
} This pattern gives clients cursor-based pagination with metadata about whether more results exist — essential for infinite scroll and "load more" UIs.
Pattern 2: Result Wrapper for Error Handling
GraphQL's error mechanism works for system-level errors, but business-level errors (e.g., "user not found") are better modeled in the type system:
union UserResult = User | NotFoundError | RateLimitError
type NotFoundError {
message: String!
resourceId: String!
}
type RateLimitError {
message: String!
retryAfter: Int!
}
type Query {
user(id: ID!): UserResult!
} Clients handle each case explicitly with fragments, making error handling type-safe and visible in the schema.
Pattern 3: Input/Output Type Separation
Never reuse an object type as both input and output. Create separate input types for mutations:
input CreateUserInput {
name: String!
email: String
}
input UpdateUserInput {
name: String
email: String
} Pattern 4: Custom Scalars for Domain Types
Elevate important domain concepts to custom scalars:
scalar DateTime
scalar URL
scalar EmailAddress
type User {
id: ID!
email: EmailAddress!
website: URL
registeredAt: DateTime!
} Custom scalars add semantic meaning and enable server-side validation at the type level rather than in resolver logic.
Applying These Patterns
Our JSON to GraphQL Converter generates the initial SDL types from your JSON samples. You can then apply these patterns on top — wrapping lists in connections, separating input/output types, and adding custom scalars — to build a production-grade schema.