Appearance
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 itemnode- The actual item (product, store, etc.)cursor- A position marker for pagination
PageInfo:
hasNextPage- More items available?hasPreviousPage- Can go backwards?endCursor- Pass toafterfor next pagestartCursor- Pass tobeforefor 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 falseID- 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 descriptionpath- Where in the query the error occurredextensions- 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
- GraphQL.org - Official documentation
- GraphQL Spec - Technical specification
- GraphQL Foundation - Community and governance
Tutorials
- How to GraphQL - Comprehensive tutorial
- Apollo GraphQL Tutorial - Client and server tutorials
- GraphQL Code Generator - Generate types from schema
Tools
- GraphQL Playground - GraphQL IDE
- Altair GraphQL Client - Desktop GraphQL client
- Apollo Client DevTools - Browser extension
Next Steps
Now that you understand GraphQL concepts:
- Tutorial: Build a Product Finder - Apply these concepts hands-on
- Explanation: Pagination Explained - Deep dive into cursor pagination
- How-to Guides: Solve specific problems with products, stores, inventory