@shopkit/data-layer
v1.1.0
Published
Enterprise data abstraction layer providing unified API for e-commerce platforms including Shopify and custom backends
Maintainers
Readme
@shopkit/data-layer
A comprehensive, production-ready data layer package for e-commerce applications, providing unified APIs for both Shopify and custom backend integrations.
Table of Contents
- Features
- Installation
- Quick Start
- Configuration
- API Reference
- Usage Examples
- Platform Support
- Error Handling
- Best Practices
- Examples
- License
Features
- 🌐 Multi-Platform Support: Seamlessly works with both Shopify and custom backend APIs
- ⚙️ Configurable Architecture: Zero environment variable dependencies - fully configurable through parameters
- 🔷 TypeScript First: Built with TypeScript for complete type safety
- 📦 Modular Design: Tree-shakable imports - use only what you need
- 🔧 Flexible Options: Support for custom queries and variables in all API methods
- 📋 Consistent Response Format: Standardized
IResponseformat across all operations - 🪵 Advanced Logging: Built-in logging with configurable levels and formatting
- 🎯 Standardized Interface: All methods follow consistent signature patterns
Installation
npm install @shopkit/data-layer
# or
yarn add @shopkit/data-layer
# or
pnpm add @shopkit/data-layerQuick Start
1. Create a Commerce Client
import { createCommerceClient, IClientConfig } from '@shopkit/data-layer';
// Shopify Client Configuration
const shopifyConfig: IClientConfig = {
type: "shopify",
config: {
domain: "your-store.myshopify.com",
storefrontAccessToken: "your-storefront-access-token",
apiVersion: "2025-04" as const,
},
logging: {
enabled: true,
level: "info",
prettyPrint: true,
name: "Shopify Data Layer"
}
};
// Custom Backend Client Configuration
const customConfig: IClientConfig = {
type: "custom",
config: {
merchantId: "your-merchant-id",
storeId: "your-store-id",
services: {
product: {
apiBaseUrl: "https://api.yourstore.com/products",
},
order: {
apiBaseUrl: "https://api.yourstore.com/orders",
},
cart: {
apiBaseUrl: "https://api.yourstore.com/cart",
},
},
},
logging: {
enabled: true,
level: "info",
prettyPrint: true,
name: "Custom Backend Data Layer"
}
};
// Create the client
const commerceClient = createCommerceClient(shopifyConfig);
// or
const commerceClient = createCommerceClient(customConfig);2. Basic Usage
// Get a product
const product = await client.getProduct({ handle: "product-handle" });
if (product.success) {
console.log(product.data);
} else {
console.error(product.error?.message);
}Configuration
Client Configuration Interfaces
// Shopify Configuration
interface IShopifyConfig {
domain: string;
storefrontAccessToken: string;
apiVersion: string;
}
// Custom Backend Configuration
interface ICustomConfig {
merchantId: string;
storeId: string;
services: {
product: { apiBaseUrl: string };
order: { apiBaseUrl: string };
cart: { apiBaseUrl: string };
};
}
// Client Configuration Union Type
type IClientConfig =
| { type: "shopify"; config: IShopifyConfig; logging?: LoggerConfig }
| { type: "custom"; config: ICustomConfig; logging?: LoggerConfig };Logging Configuration
interface LoggerConfig {
enabled: boolean;
level: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
prettyPrint: boolean;
name: string;
redact?: string[];
}Request Options
All API methods support optional configuration:
interface IOptionsRequest {
query?: string; // Custom GraphQL query
variables?: Record<string, any>; // Custom variables
headers?: Record<string, string>; // Custom headers
params?: Record<string, string>; // URL parameters
}API Reference
Method Signature Pattern
All methods follow this consistent pattern:
methodName(params: ISpecificRequest, options?: IOptionsRequest): Promise<IResponse>Standardized Response Format
interface IResponse<T = any> {
success: boolean;
message: string;
data?: T;
error?: {
message: string;
details?: any;
};
meta?: IResponseMeta;
}Product Operations
// Get single product by ID or handle
getProduct(params: ISingleProductRequest, options?: IOptionsRequest): Promise<IResponse>
interface ISingleProductRequest {
id?: string;
handle?: string;
}
// Get multiple products with filtering and pagination
getProducts(params: IProductQueryRequest, options?: IOptionsRequest): Promise<IResponse>
interface IProductQueryRequest {
first?: number;
last?: number;
after?: string;
before?: string;
query?: string;
sortKey?: string;
reverse?: boolean;
filters?: Array<{
available?: boolean;
price?: { min?: number; max?: number };
productType?: string;
vendor?: string;
tag?: string;
}>;
}
// Get multiple products by handles
getProductsByHandles(params: IProductsByHandlesRequest, options?: IOptionsRequest): Promise<IResponse>
interface IProductsByHandlesRequest {
handles: string[];
}Collection Operations
// Get single collection by ID or handle
getCollection(params: ISingleCollectionRequest, options?: IOptionsRequest): Promise<IResponse>
interface ISingleCollectionRequest {
id?: string;
handle?: string;
productLimit?: number;
productSortKey?: string;
productReverse?: boolean;
filters?: Array<{
available?: boolean;
price?: { min?: number; max?: number };
productType?: string;
vendor?: string;
tag?: string;
}>;
}
// Get multiple collections with filtering and pagination
getCollections(params: ICollectionQueryRequest, options?: IOptionsRequest): Promise<IResponse>
interface ICollectionQueryRequest {
first?: number;
last?: number;
after?: string;
before?: string;
query?: string;
sortKey?: string;
reverse?: boolean;
}
// Get multiple collections by handles with individual query parameters
getCollectionsByHandles(params: ICollectionsByhandlesRequest, options?: IOptionsRequest): Promise<IResponse>
interface ICollectionsByhandlesRequest {
handles: string[];
first?: number;
last?: number;
after?: string;
before?: string;
query?: string;
reverse?: boolean;
sortKey?: string;
productLimit?: number;
productSortKey?: string;
productReverse?: boolean;
filters?: Array<{
available?: boolean;
price?: { min?: number; max?: number };
productType?: string;
vendor?: string;
tag?: string;
}>;
}Cart Operations
// Create a new cart
createCart(options?: IOptionsRequest): Promise<IResponse>
// Get cart by ID
getCart(params: ISingleCartRequest, options?: IOptionsRequest): Promise<IResponse>
interface ISingleCartRequest {
id: string;
}
// Update cart
updateCart(params: IUpdateCartRequest, options?: IOptionsRequest): Promise<IResponse>
interface IUpdateCartRequest {
id: string;
items: ICartItemRequest;
}
interface ICartItemRequest {
productId: string;
variantId: string;
quantity: number;
sellingPlanId?: string;
attributes?: Array<{
key: string;
value: string;
}>;
}
// Add items to cart
addToCart(params: IAddToCartRequest, options?: IOptionsRequest): Promise<IResponse>
interface IAddToCartRequest {
id: string;
items: ICartItemRequest[];
}
// Remove items from cart
removeFromCart(params: IRemoveFromCartRequest, options?: IOptionsRequest): Promise<IResponse>
interface IRemoveFromCartRequest {
id: string;
itemIds: string[];
}Order Operations
// Get single order by ID (Admin only)
getOrder(params: ISingleOrderRequest, options?: IOptionsRequest): Promise<IResponse>
interface ISingleOrderRequest {
id: string;
}
// Get multiple orders with filtering (Admin only)
getOrders(params: IOrderQueryRequest, options?: IOptionsRequest): Promise<IResponse>
interface IOrderQueryRequest {
first?: number;
last?: number;
after?: string;
before?: string;
query?: string;
sortKey?: string;
reverse?: boolean;
}
// Create new order (Admin only)
createOrder(params: IOrderRequest, options?: IOptionsRequest): Promise<IResponse>
interface IOrderRequest {
lineItems: Array<{
variantId: string;
quantity: number;
}>;
customer?: {
email: string;
firstName?: string;
lastName?: string;
};
shippingAddress?: {
address1: string;
city: string;
province: string;
country: string;
zip: string;
};
}
// Update existing order (Admin only)
updateOrder(params: IUpdateOrderRequest, options?: IOptionsRequest): Promise<IResponse>
interface IUpdateOrderRequest {
id: string;
data: Partial<IOrderRequest>;
}
// Get customer order history
getOrderHistory(params: IOrderHistoryRequest, options?: IOptionsRequest): Promise<IResponse>
interface IOrderHistoryRequest {
customerAccessToken: string;
page?: number;
limit?: number;
}
// Get specific customer order
getOrderById(params: IOrderByIdRequest, options?: IOptionsRequest): Promise<IResponse>
interface IOrderByIdRequest {
customerAccessToken: string;
orderId: string;
}Customer Operations
// Get single customer by ID (Admin only)
getCustomer(params: ISingleCustomerRequest, options?: IOptionsRequest): Promise<IResponse>
interface ISingleCustomerRequest {
id: string;
}
// Get multiple customers (Admin only)
getCustomers(params: ICustomerQueryRequest, options?: IOptionsRequest): Promise<IResponse>
// Create new customer
createCustomer(params: ICustomerRequest, options?: IOptionsRequest): Promise<IResponse>
interface ICustomerRequest {
email: string;
firstName?: string;
lastName?: string;
phone?: string;
password?: string;
acceptsMarketing?: boolean;
}
// Update existing customer
updateCustomer(params: IUpdateCustomerRequest, options?: IOptionsRequest): Promise<IResponse>
interface IUpdateCustomerRequest {
id: string;
data: Partial<ICustomerRequest>;
}
// Get customer profile (Shopify only)
getCustomerProfile(params: ICustomerProfileRequest, options?: IOptionsRequest): Promise<IResponse>
interface ICustomerProfileRequest {
customerAccessToken: string;
}Checkout Operations
// Create checkout
createCheckout(params: ICreateCheckoutRequest, options?: IOptionsRequest): Promise<IResponse>
interface ICreateCheckoutRequest {
lineItems?: Array<{
variantId: string;
quantity: number;
}>;
email?: string;
shippingAddress?: {
address1: string;
city: string;
province: string;
country: string;
zip: string;
};
}Blog/Article Operations (Shopify only)
// Get single article
getArticle(params?: IGetArticleRequest, options?: IOptionsRequest): Promise<IResponse>
interface IGetArticleRequest {
blogHandle?: string;
articleHandle?: string;
first?: number;
}
// Get blog articles
getBlogArticles(params: IBlogArticleRequest, options?: IOptionsRequest): Promise<IResponse>
interface IBlogArticleRequest {
blogHandle: string;
first?: number;
after?: string;
}
// Get trending articles
getTrendingArticles(params: ITrendingArticleRequest, options?: IOptionsRequest): Promise<IResponse>
interface ITrendingArticleRequest {
first?: number;
sortBy?: string;
}
// Get article by handle
getArticleByHandle(params: IArticleByHandleRequest, options?: IOptionsRequest): Promise<IResponse>
interface IArticleByHandleRequest {
blogHandle: string;
articleHandle: string;
}Utility Operations
// Custom GraphQL request (Shopify only)
graphqlRequest(params: IGraphqlRequest): Promise<IResponse>
interface IGraphqlRequest {
query: string;
variables?: Record<string, any>;
}
// Custom HTTP fetch request
fetchRequest(params: IFetchRequest): Promise<IResponse>
interface IFetchRequest {
url: string;
method?: string;
headers?: Record<string, string>;
body?: string;
signal?: AbortSignal;
[key: string]: any;
}Usage Examples
Product Operations
// Get single product
const product = await client.getProduct({ handle: "product-handle" });
// Get single product with custom headers
const product = await client.getProduct(
{ handle: "product-handle" },
{ headers: { "X-Custom-Header": "value" } }
);
// Get multiple products
const products = await client.getProducts({
first: 20,
query: "shirt",
sortKey: "TITLE"
});
// Get products by handles
const products = await client.getProductsByHandles({
handles: ["handle1", "handle2"]
});Collection Operations
// Get single collection
const collection = await client.getCollection({ handle: "collection-handle" });
// Get multiple collections
const collections = await client.getCollections({ first: 10 });
// Get collections by handles with shared parameters
const collections = await client.getCollectionsByHandles({
handles: ["featured-products", "sale-items", "new-arrivals"],
first: 10,
productLimit: 20,
filters: [{ available: true }]
});Cart Operations
// Create cart
const cart = await client.createCart();
// Get cart
const cart = await client.getCart({ id: "cart-id" });
// Add items to cart
const updatedCart = await client.addToCart({
id: "cart-id",
items: [
{
productId: "product-id",
variantId: "variant-id",
quantity: 1
}
]
});
// Update cart
const updatedCart = await client.updateCart({
id: "cart-id",
data: {
items: [
{
productId: "product-id",
variantId: "variant-id",
quantity: 2
}
]
}
});
// Remove items from cart
const updatedCart = await client.removeFromCart({
id: "cart-id",
itemIds: ["line-id"]
});Order Operations
// Get order history
const orders = await client.getOrderHistory({
customerAccessToken: "customer-access-token",
page: 1,
limit: 10
});
// Get specific order
const order = await client.getOrderById({
customerAccessToken: "customer-access-token",
orderId: "order-id"
});Framework Integration Examples
Next.js App Router (Server-Side)
// app/products/[handle]/page.tsx
import { createCommerceClient } from '@shopkit/data-layer';
export default async function ProductPage({ params }: { params: { handle: string } }) {
const client = createCommerceClient(shopifyConfig);
const response = await client.getProduct({ handle: params.handle });
if (!response.success) {
return <div>Error: {response.error?.message}</div>;
}
const product = response.data;
return (
<div>
<h1>{product.title}</h1>
<p>{product.description}</p>
<div>Price: {product.priceRange.minVariantPrice.amount} {product.priceRange.minVariantPrice.currencyCode}</div>
</div>
);
}API Route Usage
// app/api/products/route.ts
import { NextRequest } from 'next/server';
import { createCommerceClient } from '@shopkit/data-layer';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const client = createCommerceClient(shopifyConfig);
const response = await client.getProducts({
first: 10,
query: searchParams.get('query') || undefined
});
return Response.json(response);
}Client-Side Usage
'use client';
import { useState, useEffect } from 'react';
import { IResponse } from '@shopkit/data-layer';
export function useProducts(query?: string) {
const [response, setResponse] = useState<IResponse | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchProducts() {
try {
const res = await fetch(`/api/products?query=${query || ''}`);
const data = await res.json();
setResponse(data);
} catch (error) {
setResponse({
success: false,
message: 'Failed to fetch products',
error: { message: error instanceof Error ? error.message : 'Unknown error' }
});
} finally {
setLoading(false);
}
}
fetchProducts();
}, [query]);
return { response, loading };
}Platform Support
Shopify Adapter
- ✅ Products: Full support for product operations
- ✅ Collections: Full support for collection operations
- ✅ Cart: Full cart management
- ✅ Orders: Customer order history and details
- ✅ Blog: Article and blog operations
- ✅ GraphQL: Custom GraphQL queries
- ✅ Customer: Customer profile access
Custom Backend Adapter
- ✅ Products: Full support for product operations
- ✅ Collections: Full support for collection operations
- ✅ Cart: Full cart management
- ✅ Orders: Customer order history and details
- ✅ Checkout: Basic checkout creation
- ❌ Blog: Not implemented
- ❌ GraphQL: Not available
Method Support Matrix
| Method | Custom Adapter | Shopify Adapter | Notes |
|--------|----------------|-----------------|-------|
| Product Operations |
| getProduct | ✅ Implemented | ✅ Implemented | Core product retrieval |
| getProducts | ✅ Implemented | ✅ Implemented | Product listing with filters |
| getProductsByHandles | ✅ Implemented | ✅ Implemented | Batch product retrieval |
| Collection Operations |
| getCollection | ✅ Implemented | ✅ Implemented | Single collection retrieval |
| getCollections | ✅ Implemented | ✅ Implemented | Collection listing |
| getCollectionsByHandles | ✅ Implemented | ✅ Implemented | Batch collection retrieval |
| Order Operations |
| getOrder | ❌ (Error thrown) | ❌ (NotImplemented) | Single order retrieval |
| getOrders | ❌ (Error thrown) | ❌ (NotImplemented) | Order listing |
| createOrder | ❌ (Error thrown) | ❌ (NotImplemented) | Order creation |
| updateOrder | ❌ (Error thrown) | ❌ (NotImplemented) | Order updates |
| getOrderHistory | ✅ Implemented | ✅ Implemented | Customer order history |
| getOrderById | ✅ Implemented | ✅ Implemented | Customer specific order |
| Customer Operations |
| getCustomer | ❌ (Error thrown) | ❌ (NotImplemented) | Single customer retrieval |
| getCustomers | ❌ (Error thrown) | ❌ (NotImplemented) | Customer listing |
| createCustomer | ❌ (Error thrown) | ❌ (NotImplemented) | Customer creation |
| updateCustomer | ❌ (Error thrown) | ❌ (NotImplemented) | Customer updates |
| getCustomerProfile | ❌ (Not available) | ✅ Implemented | Shopify-specific method |
| Cart Operations |
| getCart | ✅ Implemented | ✅ Implemented | Cart retrieval |
| createCart | ✅ Implemented | ✅ Implemented | Cart creation |
| updateCart | ✅ Implemented | ✅ Implemented | Cart updates |
| addToCart | ✅ Implemented | ✅ Implemented | Add items to cart |
| removeFromCart | ✅ Implemented | ✅ Implemented | Remove items from cart |
| Checkout Operations |
| createCheckout | ✅ Implemented | ❌ (NotImplemented) | Checkout creation |
| Article/Blog Operations |
| getArticle | ❌ (NotImplemented) | ✅ Implemented | Single article retrieval |
| getBlogArticles | ❌ (NotImplemented) | ✅ Implemented | Blog article listing |
| getTrendingArticles | ❌ (NotImplemented) | ✅ Implemented | Trending articles |
| getArticleByHandle | ❌ (NotImplemented) | ✅ Implemented | Article by handle |
| Utility Operations |
| graphqlRequest | ❌ (NotImplemented) | ✅ Implemented | GraphQL utility |
| fetchRequest | ✅ Inherited | ✅ Inherited | HTTP fetch utility |
Error Handling
The data layer provides comprehensive error handling:
const response = await client.getProduct({ handle: "invalid-handle" });
if (!response.success) {
console.error("Error:", response.error?.message);
// Handle specific error types
if (response.error?.details?.name === "NotFoundError") {
// Handle product not found
}
}Best Practices
- Always check
response.successbefore accessingdata - Use TypeScript for full type safety and better developer experience
- Enable logging during development for better debugging
- Handle errors gracefully with proper fallbacks
- Use environment variables for sensitive configuration data
- Implement proper caching for frequently accessed data
- Use custom queries sparingly and only when standard methods are insufficient
- Follow the standardized method signature pattern for consistency
Examples
For complete examples and usage patterns, see the data layer example app.
Data Layer Example App Features
The example application includes several user experience improvements:
Enhanced JSON Input Fields
The example app provides improved textarea formatting for complex JSON payloads:
- Add to Cart: Multi-line textarea with formatted JSON structure for items array
- Update Cart: Enhanced textarea for cart item modifications with proper formatting
- Create Checkout: Improved textarea for checkout payload with correct interface structure
Example Add to Cart Payload Format:
[
{
"productId": "gid://shopify/Product/123",
"variantId": "gid://shopify/ProductVariant/123",
"quantity": 1,
"sellingPlanId": "selling-plan-id",
"attributes": [
{
"key": "gift-wrap",
"value": "true"
}
]
}
]Example Create Checkout Payload Format:
{
"cart_token": "cart-token-here",
"checkout_id": "checkout-id-here",
"attributes": {
"email": "[email protected]",
"note": "Special instructions",
"phone": "+1234567890",
"custom_field": "custom_value"
}
}These improvements provide:
- Better Readability: Multi-line JSON with proper indentation
- Larger Input Areas: Textarea fields instead of single-line inputs
- Complete Examples: Shows all available fields and proper structure
- Error Prevention: Properly formatted examples reduce JSON syntax errors
License
MIT License - see the LICENSE file for details.
