Skip to content

GraphQL Concepts

This guide explains GraphQL fundamentals and how LCBO.dev uses them. Whether you're new to GraphQL or want to deepen your understanding, this will help you make the most of the API.

What is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries. Created by Facebook in 2012 and open-sourced in 2015, it provides an alternative approach to REST.

Key Characteristics

Query Language

  • You describe the data you want
  • The server returns exactly that data
  • No more, no less

Strongly Typed

  • Every piece of data has a defined type
  • Queries are validated against the schema
  • Errors are caught before execution

Single Endpoint

  • One URL for everything: /graphql
  • No need to remember multiple endpoints
  • Simpler API surface

Introspectable

  • The schema describes itself
  • Tools can automatically generate documentation
  • GraphQL IDEs provide auto-complete

Queries vs Mutations

Queries (Read Operations)

Queries fetch data without modifying it:

graphql
query {
  products(pagination: { first: 5 }) {
    edges {
      node {
        name
        priceInCents
      }
    }
  }
}

LCBO.dev is read-only, so you'll only use queries (no mutations).

Mutations (Write Operations)

Mutations modify data on the server:

graphql
mutation {
  createProduct(input: { name: "New Product" }) {
    id
    name
  }
}

The LCBO.dev API doesn't support mutations since it provides read-only access to LCBO data.

Fields and Selection Sets

Selecting Fields

You specify exactly which fields you want:

graphql
query {
  product(sku: "438663") {
    name          # Request the name
    priceInCents  # Request the price
    # Don't request other fields like producer, origin, etc.
  }
}

Response contains only requested fields:

json
{
  "data": {
    "product": {
      "name": "Corona Extra",
      "priceInCents": 1295
    }
  }
}

Nested Fields

GraphQL excels at fetching related data:

graphql
query {
  product(sku: "438663") {
    name
    inventories {          # Nested: product's inventories
      quantity
      store {              # Nested further: inventory's store
        name
        city
      }
    }
  }
}

This would require 3+ REST API calls, but GraphQL does it in one query.

Arguments

Fields can accept arguments to customize their behavior:

graphql
query {
  products(
    filters: { categorySlug: "wine" }  # Filter argument
    pagination: { first: 10 }          # Pagination argument
    sortBy: PRICE                      # Sort argument
  ) {
    edges {
      node {
        name
      }
    }
  }
}

Arguments are typed and validated by the schema.

Variables

Instead of hardcoding values, use variables:

graphql
query GetProduct($sku: String!) {  # Declare variable with type
  product(sku: $sku) {              # Use variable
    name
    priceInCents
  }
}

Pass variables separately:

json
{
  "sku": "438663"
}

Benefits:

  • Type safety: GraphQL validates variable types
  • Security: Prevents injection attacks
  • Reusability: Same query, different values
  • Cleaner: Separates query logic from data

Variable Types

Variables have types that match the schema:

graphql
query SearchProducts(
  $search: String!           # Required string
  $maxPriceInCents: Int      # Optional integer
  $isSeasonal: Boolean       # Optional boolean
) {
  products(
    filters: {
      search: $search
      maxPriceInCents: $maxPriceInCents
      isSeasonal: $isSeasonal
    }
  ) {
    edges {
      node {
        name
      }
    }
  }
}
  • ! means required (must be provided)
  • No ! means optional (can be null)

Aliases

Query the same field multiple times with different arguments:

graphql
query {
  cheapWines: products(
    filters: { categorySlug: "wine", maxPriceInCents: 1500 }
    pagination: { first: 5 }
  ) {
    edges {
      node {
        name
        priceInCents
      }
    }
  }

  expensiveWines: products(
    filters: { categorySlug: "wine", minPriceInCents: 5000 }
    pagination: { first: 5 }
  ) {
    edges {
      node {
        name
        priceInCents
      }
    }
  }
}

Response has both:

json
{
  "data": {
    "cheapWines": { ... },
    "expensiveWines": { ... }
  }
}

Use cases:

  • Homepage sections (featured, new arrivals, top sellers)
  • Comparison views
  • Dashboard widgets

Fragments

Reuse field selections across queries:

graphql
fragment ProductBasics on Product {
  sku
  name
  priceInCents
  producerName
}

fragment StoreBasics on Store {
  externalId
  name
  address
  city
}

query {
  product(sku: "438663") {
    ...ProductBasics      # Use fragment
    origin                # Add more fields
    inventories {
      quantity
      store {
        ...StoreBasics  # Reuse in nested query
      }
    }
  }
}

Benefits:

  • Consistency: Same fields everywhere
  • Maintainability: Update once, apply everywhere
  • Readability: Cleaner queries
  • Client libraries: Apollo and Relay have built-in fragment support

Connections and Edges

LCBO.dev uses the Relay connection pattern for pagination. Understanding this pattern helps you work with lists of data.

The Connection Pattern

A "connection" represents a paginated list:

graphql
query {
  products(pagination: { first: 10 }) {
    edges {              # List of edges
      node {             # The actual product
        sku
        name
      }
      cursor             # Position marker
    }
    pageInfo {           # Pagination metadata
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
    totalCount          # Total items available
  }
}

Why This Structure?

Edges and Nodes:

  • edge - A relationship between the list and an item
  • node - The actual item (product, store, etc.)
  • cursor - A position marker for pagination

PageInfo:

  • hasNextPage - More items available?
  • hasPreviousPage - Can go backwards?
  • endCursor - Pass to after for next page
  • startCursor - Pass to before for previous page

For complete field definitions and types, see the PageInfo reference.

Why Not Just an Array?

The connection pattern enables:

  • Cursor-based pagination (stable, efficient)
  • Metadata about pagination state
  • Edge-specific fields (like distance in geospatial queries)
  • Standard pattern across GraphQL APIs

Example: Distance in Edges

For store queries with location, distanceKm is on the edge (not the node):

graphql
query {
  stores(
    filters: { latitude: 43.6532, longitude: -79.3832, radiusKm: 10 }
    pagination: { first: 5 }
  ) {
    edges {
      node {
        name       # Store data
        address
      }
      # distanceKm could be here (edge-level) if computed per-query
    }
  }
}

Note: LCBO.dev includes distanceKm in the store node for convenience.

Pagination Patterns

Forward Pagination

Get the first N items, then load more:

graphql
# First page
query {
  products(pagination: { first: 20 }) {
    edges {
      node { sku name }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

If hasNextPage is true, load more:

graphql
# Next page
query {
  products(pagination: {
    first: 20
    after: "cursor_from_previous_response"
  }) {
    edges {
      node { sku name }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Backward Pagination

Get the last N items:

graphql
query {
  products(pagination: { last: 20 }) {
    edges {
      node { sku name }
    }
    pageInfo {
      hasPreviousPage
      startCursor
    }
  }
}

Load previous:

graphql
query {
  products(pagination: {
    last: 20
    before: "cursor_from_previous_response"
  }) {
    edges {
      node { sku name }
    }
  }
}

See Pagination Explained for detailed discussion of cursor vs offset pagination.

Schema and Types

The GraphQL schema defines what's possible:

Scalar Types

Built-in primitive types:

  • String - Text (e.g., "Corona Extra")
  • Int - Whole numbers (e.g., 1295)
  • Float - Decimals (e.g., 12.5)
  • Boolean - true or false
  • ID - Unique identifier (often a string)

Object Types

Complex types with fields:

graphql
type Product {
  sku: String!
  name: String!
  priceInCents: Int!
  producerName: String
  alcoholPercent: Float
  isSeasonal: Boolean!
}

Input Types

For arguments and variables:

graphql
input ProductFilterInput {
  categorySlug: String
  minPriceInCents: Int
  maxPriceInCents: Int
  isSeasonal: Boolean
}

Enum Types

Predefined set of values:

graphql
enum SortDirection {
  ASC
  DESC
}

enum ProductSortField {
  NAME
  PRICE
  ALCOHOL_PERCENT
}

Non-Null Types

! means the field cannot be null:

graphql
type Product {
  sku: String!        # Always present
  name: String!       # Always present
  origin: String      # Might be null
}

Introspection

GraphQL schemas are introspectable - you can query the schema itself:

graphql
query {
  __schema {
    types {
      name
      description
    }
  }
}

Or query a specific type:

graphql
query {
  __type(name: "Product") {
    name
    fields {
      name
      type {
        name
      }
    }
  }
}

Tools use introspection to:

  • Generate documentation automatically
  • Provide IDE auto-complete
  • Validate queries
  • Generate type definitions

Error Handling

GraphQL has a standardized error format:

json
{
  "errors": [
    {
      "message": "Product not found",
      "path": ["product"],
      "extensions": {
        "code": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "product": null
  }
}

Error Fields:

  • message - Human-readable description
  • path - Where in the query the error occurred
  • extensions - Additional metadata (error codes, etc.)

Partial Success:

GraphQL can return partial data with errors:

json
{
  "errors": [
    {
      "message": "Store not found",
      "path": ["stores", "edges", 2, "node"]
    }
  ],
  "data": {
    "stores": {
      "edges": [
        { "node": { ... } },  // Success
        { "node": { ... } },  // Success
        { "node": null }      // Error (see errors array)
      ]
    }
  }
}

Best Practices

Request Only What You Need

graphql
# ✅ Good - minimal fields
query {
  products(pagination: { first: 100 }) {
    edges {
      node {
        sku
        name
        priceInCents
      }
    }
  }
}

# ❌ Avoid - requesting everything
query {
  products(pagination: { first: 100 }) {
    edges {
      node {
        sku
        name
        priceInCents
        producerName
        origin
        description
        thumbnailUrl
        imageUrl
        # ... and 20 more fields
      }
    }
  }
}

Smaller payloads = faster responses = better UX.

Use Variables

graphql
# ✅ Good - parameterized
query GetProduct($sku: String!) {
  product(sku: $sku) {
    name
  }
}

# ❌ Avoid - hardcoded
query {
  product(sku: "438663") {
    name
  }
}

Variables provide validation and prevent injection attacks.

Use Fragments for Reusability

graphql
# ✅ Good - DRY principle
fragment ProductCard on Product {
  sku
  name
  priceInCents
  thumbnailUrl
}

query {
  featured: products(filters: { featured: true }) {
    edges {
      node {
        ...ProductCard
      }
    }
  }
  
  onSale: products(filters: { onSale: true }) {
    edges {
      node {
        ...ProductCard
      }
    }
  }
}

Name Your Queries

graphql
# ✅ Good - named query
query GetNearbyStores($lat: Float!, $lng: Float!) {
  stores(filters: { latitude: $lat, longitude: $lng, radiusKm: 10 }) {
    edges {
      node {
        name
      }
    }
  }
}

# ❌ Avoid - anonymous query
query {
  stores(filters: { latitude: 43.6532, longitude: -79.3832, radiusKm: 10 }) {
    edges {
      node {
        name
      }
    }
  }
}

Named queries are easier to debug and monitor.

Learning Resources

Official Resources

Tutorials

Tools

Next Steps

Now that you understand GraphQL concepts: