GraphQL API
EverShop uses GraphQL for data fetching in React components. Extensions can add new types, extend existing types, and provide resolvers to fetch data from any source.
What is GraphQL in EverShop?
GraphQL provides:
Type-safe data fetching for React components
Flexible queries that request exactly the data needed
Extensible schema that can be augmented by any extension
Automatic data loading for page components
Resolver functions that can fetch from databases, APIs, or any data source
Every page component can export a query that automatically fetches data and passes it as props to the component.
Schema Files
GraphQL schemas are defined in .graphql files within extensions:
extensions/[name]/src/graphql/types/[TypeName]/
├── TypeName.graphql # Type definitions
└── TypeName.resolvers.js # Resolver functions
Creating GraphQL Types
Define the GraphQL schema
Create a .graphql file with your type definitions:
type Foo {
id : ID !
name : String !
description : String
}
type Query {
foo ( id : ID ! ): Foo
foos : [ Foo ! ] !
}
Implement the resolver functions:
const fooList = [
{ id: 1 , name: 'Foo' , description: 'This is a Foo object' },
{ id: 2 , name: 'Bar' , description: 'This is a Bar object' },
{ id: 3 , name: 'Baz' , description: 'This is a Baz object' }
];
export default {
Query: {
foo : ( root , { id }) => {
return fooList . find (( foo ) => foo . id === id );
},
foos : () => {
return fooList ;
}
} ,
Foo: {
id : ( foo ) => foo . id ,
name : ( foo ) => foo . name ,
description : ( foo ) => foo . description
}
} ;
Query the data in your page components:
import React from 'react' ;
type FooListProps = {
foos ?: {
id : number ;
name : string ;
description : string ;
}[];
};
export default function FooList ({ foos } : FooListProps ) {
return (
< div className = "foo-list container mx-auto px-4 py-8" >
< h2 className = "font-bold text-center mb-8" > Foo List </ h2 >
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" >
{ foos ?. map (( foo ) => (
< div key = { foo . id } className = "bg-white rounded-lg shadow-md p-6" >
< h3 className = "font-semibold mb-3" > { foo . name } </ h3 >
< p className = "text-gray-600" > { foo . description } </ p >
</ div >
)) }
</ div >
</ div >
);
}
export const layout = {
areaId: 'content' ,
sortOrder: 30
};
export const query = `
query Query {
foos {
id
name
description
}
}
` ;
Extending Existing Types
You can add fields to types defined by other extensions or the core system:
extensions/productCatalog/src/graphql/types/ProductExtension/ProductExtension.graphql
type Supplement {
ingredients : String
benefits : [ String ]
presentation : String
dosage : String
warnings : String
storage : String
}
type ProductExtension {
supplement : Supplement
}
extend type Product {
extension : ProductExtension
}
Use extend type TypeName to add fields to an existing type. The base type must be defined elsewhere (core or another extension).
Resolvers for extended types:
extensions/productCatalog/src/graphql/types/ProductExtension/ProductExtension.resolvers.js
export default {
Product: {
extension : ( product ) => {
return {
supplement: {
ingredients: product . ingredients || null ,
benefits: product . benefits ? JSON . parse ( product . benefits ) : null ,
presentation: product . presentation || null ,
dosage: product . dosage || null ,
warnings: product . warnings || null ,
storage: product . storage || null
}
};
}
}
} ;
Resolver Structure
Resolvers are organized by type and field:
export default {
// Root Query resolvers
Query: {
foo : ( root , args , context ) => {
// Fetch single foo by ID
},
foos : ( root , args , context ) => {
// Fetch list of foos
}
} ,
// Root Mutation resolvers
Mutation: {
createFoo : ( root , args , context ) => {
// Create new foo
}
} ,
// Type field resolvers
Foo: {
id : ( parent ) => parent . id ,
name : ( parent ) => parent . name ,
// Computed field
fullDescription : ( parent ) => {
return ` ${ parent . name } : ${ parent . description } ` ;
}
}
} ;
Resolver Arguments
Every resolver receives four arguments:
( parent , args , context , info ) => {
// parent: The result from the parent resolver
// args: Arguments passed to the field
// context: Shared context (request, database, etc.)
// info: Query metadata (rarely used)
}
Query Resolver
Field Resolver
Nested Resolver
Query : {
foo : ( root , { id }, context ) => {
// root: Usually null for Query
// id: Argument from query foo(id: "123")
// context: { request, db, ... }
return context . db . findFooById ( id );
}
}
Query Export in Components
Page components can export a GraphQL query that runs automatically:
export const query = `
query Query {
product(id: getContextValue("productId")) {
productId
name
price
description
}
}
` ;
Context Values
Use getContextValue() to access runtime values:
query Query {
product ( id : getContextValue ( "productId" )) {
name
}
user ( id : getContextValue ( "userId" )) {
email
}
}
URL Generation
Get URLs for routes:
query Query {
loginUrl : url ( routeId : "login" )
productUrl : url ( routeId : "product" , params : { id : "123" })
}
Real-World Example: Product Reviews
From the productReviews extension:
import React from 'react' ;
interface Review {
id : string ;
author : string ;
rating : number ;
comment : string ;
createdAt : string ;
}
type ProductReviewsProps = {
product : {
productId : string ;
name : string ;
};
reviews ?: Review [];
action ?: string ;
};
export default function ProductReviews ({
product ,
reviews = [],
action
} : ProductReviewsProps ) {
const averageRating = reviews . length > 0
? reviews . reduce (( sum , r ) => sum + r . rating , 0 ) / reviews . length
: 0 ;
return (
< div className = "bg-[#F8FAF9] border border-[#E8F5E9] rounded-lg p-6 mt-6" >
< h3 className = "text-xl font-bold text-[#2D5A3D] mb-6" >
Reseñas de Clientes
</ h3 >
{ reviews . length > 0 && (
< div className = "mb-6" >
< span className = "text-[#4A5568]" >
{ averageRating . toFixed ( 1 ) } de 5 ( { reviews . length } reseñas)
</ span >
</ div >
) }
< div className = "space-y-4" >
{ reviews . map (( review ) => (
< div key = { review . id } className = "border-b border-[#E8F5E9] pb-4" >
< span className = "font-medium text-[#2D5A3D]" > { review . author } </ span >
< p className = "text-[#4A5568] text-sm" > { review . comment } </ p >
</ div >
)) }
</ div >
</ div >
);
}
export const layout = {
areaId: 'productPageBottom' ,
sortOrder: 10
};
export const query = `
query Query {
product(id: getContextValue("productId")) {
productId
name
reviews {
id
author
rating
comment
createdAt
}
}
action: url(routeId: "productReviews")
}
` ;
Scalar Types
GraphQL supports several scalar types:
Type Description Example IDUnique identifier "123", "abc-def"StringText value "Hello World"IntInteger number 42, -10FloatDecimal number 3.14, -0.5BooleanTrue or false true, false
Non-null Types
Use ! to mark fields as required:
type Product {
id : ID ! # Required ID
name : String ! # Required String
description : String # Optional String
price : Float ! # Required Float
}
Lists
Use [] for arrays:
type Product {
tags : [ String ] # Optional array of optional strings
tags : [ String ] ! # Required array of optional strings
tags : [ String ! ] ! # Required array of required strings
}
Mutations
Mutations modify data:
type Mutation {
createReview ( input : ReviewInput ! ): Review !
updateReview ( id : ID ! , input : ReviewInput ! ): Review !
deleteReview ( id : ID ! ): Boolean !
}
input ReviewInput {
author : String !
rating : Int !
comment : String !
}
With resolvers:
export default {
Mutation: {
createReview : async ( root , { input }, context ) => {
const { db } = context ;
const result = await db . query (
'INSERT INTO reviews (author_name, rating, comment) VALUES ($1, $2, $3) RETURNING *' ,
[ input . author , input . rating , input . comment ]
);
return {
id: result . rows [ 0 ]. id ,
author: result . rows [ 0 ]. author_name ,
rating: result . rows [ 0 ]. rating ,
comment: result . rows [ 0 ]. comment ,
createdAt: result . rows [ 0 ]. created_at . toISOString ()
};
}
}
} ;
Best Practices
Each resolver should do one thing well: // Good: Simple, focused resolver
Query : {
products : ( root , args , context ) => {
return context . db . getAllProducts ();
}
}
// Avoid: Resolver doing too much
Query : {
products : async ( root , args , context ) => {
const products = await context . db . getAllProducts ();
const reviews = await context . db . getAllReviews ();
const categories = await context . db . getAllCategories ();
// ... complex data manipulation
return complexResult ;
}
}
Use TypeScript for type safety
Define types for your GraphQL data: interface Foo {
id : string ;
name : string ;
description ?: string ;
}
const fooList : Foo [] = [
{ id: '1' , name: 'Foo' , description: 'A foo object' }
];
Use DataLoader or batch queries to prevent performance issues: // Bad: N+1 query problem
Product : {
reviews : ( product , args , context ) => {
// This runs once PER product!
return context . db . getReviewsForProduct ( product . id );
}
}
// Better: Use DataLoader
Product : {
reviews : ( product , args , context ) => {
return context . reviewLoader . load ( product . id );
}
}
Add descriptions to types and fields: """A foo represents..."""
type Foo {
"""Unique identifier for the foo"""
id : ID !
"""Display name of the foo"""
name : String !
"""Optional description providing more details"""
description : String
}
Testing GraphQL Queries
Using GraphQL Playground
EverShop includes GraphQL Playground (usually at http://localhost:3000/graphql):
query GetFoos {
foos {
id
name
description
}
}
query GetFoo {
foo ( id : "1" ) {
id
name
description
}
}
mutation CreateReview {
createReview ( input : {
author : "John Doe"
rating : 5
comment : "Great product!"
}) {
id
author
rating
}
}
Using curl
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "{ foos { id name } }"
}'
Next Steps
Learn about Extensions for organizing your GraphQL code
Understand Routing for REST API alternatives
Explore Themes for using GraphQL in page components