Loading...
Loading...
Apply when deciding, designing, or implementing data fetching with FastStore GraphQL files in src/graphql/ or src/fragments/, or configuring faststore.config. Covers API extensions, GraphQL fragments, server-side and client-side data fetching, and custom resolver patterns. Use for integrating custom data sources or extending the FastStore GraphQL schema.
npx skill4agent add vtexdocs/ai-skills faststore-data-fetchingsrc/graphql/vtex/src/graphql/thirdParty/src/fragments/faststore-state-managementfaststore-themingfaststore-overridessrc/graphql/vtex/src/graphql/thirdParty/src/fragments/ServerProduct.tssrc/fragments/ClientProduct.tsNEXT_PUBLIC_pages/api//api/catalog//api/catalog_system/fetch('https://{account}.vtexcommercestable.com.br/api/catalog')fetch('https://{account}.myvtex.com/api/catalog')src/graphql/axiosfetchsrc/components/src/pages/// src/graphql/vtex/resolvers/product.ts
// Server-side resolver — REST calls to VTEX APIs are correct here
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
customAttribute: async (root, _args, context) => {
// Server-side: safe to call VTEX REST APIs in resolvers
const response = await context.clients.commerce.catalog.getProduct(
root.productID
)
return response.customAttribute
},
},
}
export default productResolver// src/components/ProductCustomData.tsx
// WRONG: Direct REST call to VTEX Catalog API from a client component
import React, { useEffect, useState } from 'react'
interface ProductCustomDataProps {
productId: string
}
export default function ProductCustomData({ productId }: ProductCustomDataProps) {
const [data, setData] = useState(null)
useEffect(() => {
// WRONG: Direct REST call from the browser
// This exposes the VTEX domain, bypasses caching, and creates CORS issues.
fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`)
.then((res) => res.json())
.then(setData)
}, [productId])
return <div>{data?.Name}</div>
}VTEX_APP_KEYVTEX_APP_TOKENNEXT_PUBLIC_VTEX_APP_KEYVTEX_APP_TOKENX-VTEX-API-AppKeyX-VTEX-API-AppTokensrc/components/src/pages/NEXT_PUBLIC_VTEX_APP_KEYNEXT_PUBLIC_VTEX_APP_TOKEN.envNEXT_PUBLIC_// src/graphql/vtex/resolvers/installments.ts
// API keys are used ONLY in server-side resolvers, accessed via context
import type { Resolver } from '@faststore/api'
const installmentResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
// context.clients handles authentication automatically
// No API keys are hardcoded or exposed
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments = product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default installmentResolver// src/components/ProductInstallments.tsx
// CRITICAL SECURITY ISSUE: API keys exposed in client-side code
import React, { useEffect, useState } from 'react'
export default function ProductInstallments({ productId }: { productId: string }) {
const [installments, setInstallments] = useState([])
useEffect(() => {
fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`, {
headers: {
// CRITICAL: These keys are now visible to EVERY visitor of your site.
// Anyone can extract them from the browser's network tab.
'X-VTEX-API-AppKey': 'vtexappkey-mystore-ABCDEF',
'X-VTEX-API-AppToken': 'very-secret-token-12345',
},
})
.then((res) => res.json())
.then((data) => setInstallments(data.Installments))
}, [productId])
return <div>{installments.length} installments available</div>
}src/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/.graphqlsrc/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/# src/graphql/vtex/typeDefs/product.graphql
type StoreProduct {
availableInstallments: [Installment]
}
type Installment {
count: Int
value: Float
totalValue: Float
interestRate: Float
}// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments =
product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default productResolver// src/graphql/vtex/resolvers/index.ts
import { default as StoreProductResolver } from './product'
const resolvers = {
...StoreProductResolver,
}
export default resolvers// WRONG: Resolver placed in src/api/ instead of src/graphql/vtex/resolvers/
// src/api/resolvers/product.ts
// This file will NOT be discovered by FastStore's build system.
// The GraphQL schema will NOT include the extended fields.
// Components querying these fields will get runtime errors.
const productResolver = {
StoreProduct: {
availableInstallments: async (root: any) => {
return []
},
},
}
export default productResolversrc/
├── graphql/
│ ├── vtex/
│ │ ├── typeDefs/
│ │ │ └── product.graphql
│ │ └── resolvers/
│ │ ├── product.ts
│ │ └── index.ts
│ └── thirdParty/
│ ├── typeDefs/
│ │ └── extra.graphql
│ └── resolvers/
│ ├── reviews.ts
│ └── index.ts
└── fragments/
├── ServerProduct.ts ← server-side fragment (SSR)
└── ClientProduct.ts ← client-side fragment (post-render)StoreProduct# src/graphql/vtex/typeDefs/product.graphql
type StoreProduct {
availableInstallments: [Installment]
}
type Installment {
count: Int!
value: Float!
totalValue: Float!
interestRate: Float!
}// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'
const productResolver: Record<string, Resolver> = {
StoreProduct: {
availableInstallments: async (root, _args, context) => {
const product = await context.clients.commerce.catalog.getProduct(
root.productID
)
const installments =
product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []
return installments.map((inst: any) => ({
count: inst.NumberOfInstallments,
value: inst.Value,
totalValue: inst.TotalValuePlusInterestRate,
interestRate: inst.InterestRate,
}))
},
},
}
export default productResolver// src/fragments/ServerProduct.ts
import { gql } from '@faststore/core/api'
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
count
value
totalValue
interestRate
}
}
}
`)// src/graphql/thirdParty/resolvers/reviews.ts
import type { Resolver } from '@faststore/api'
const REVIEWS_API_KEY = process.env.REVIEWS_API_KEY // Server-only env var (no NEXT_PUBLIC_ prefix)
const reviewsResolver: Record<string, Resolver> = {
StoreProduct: {
reviews: async (root) => {
const response = await fetch(
`https://api.reviews-service.com/products/${root.productID}/reviews`,
{
headers: {
Authorization: `Bearer ${REVIEWS_API_KEY}`,
},
}
)
const data = await response.json()
return {
averageRating: data.average_rating,
totalReviews: data.total_count,
reviews: data.reviews.slice(0, 5).map((r: any) => ({
author: r.author_name,
rating: r.rating,
text: r.review_text,
date: r.created_at,
})),
}
},
},
}
export default reviewsResolverVTEX_APP_KEYVTEX_APP_TOKENNEXT_PUBLIC_src/graphql/vtex/src/graphql/thirdParty/pages/api/src/graphql/vtex/resolvers/index.tssrc/graphql/vtex/src/graphql/thirdParty/typeDefs/resolvers/NEXT_PUBLIC_src/fragments/faststore-state-management