Reusable Shopify app utilities — Polaris components, React hooks, server middleware, webhook handlers, and helper functions.
Add .npmrc to your project root:
@ashishnitw:registry=https://npm.pkg.github.com
Then install:
npm install @ashishnitw/shopify-utils@ashishnitw/shopify-utils
├── components/ # Polaris-based React UI components (ESM)
├── hooks/ # React hooks for Shopify apps (ESM)
├── server/ # Server-side utilities (CommonJS)
│ ├── api/ # Shopify REST & GraphQL clients, bulk operations
│ ├── middleware/ # Auth verification, session management, error handling, rate limiting, CSP
│ └── webhooks/ # Webhook verification, registration, handlers, GDPR
└── utils/ # Shared helpers (CommonJS) — logging, currency, dates, GraphQL, validators, errors
// Root — server + utils combined (CommonJS)
const { shopifyGet, formatMoney, createLogger } = require('@ashishnitw/shopify-utils');
// Specific sub-packages
import { IndexTable, Page, Toast } from '@ashishnitw/shopify-utils/components';
import { useFetch, usePagination, useForm } from '@ashishnitw/shopify-utils/hooks';
const { shopifyGraphQL, createBulkQuery } = require('@ashishnitw/shopify-utils/server/api');
const { verifyShopifySession, shopifyCSP } = require('@ashishnitw/shopify-utils/server/middleware');
const { createWebhookRouter, createGDPRRouter } = require('@ashishnitw/shopify-utils/server/webhooks');
const { formatMoney, timeAgo, isValidShopDomain } = require('@ashishnitw/shopify-utils/utils');All components are React (JSX) and wrap Shopify Polaris primitives.
| Component | Description |
|---|---|
IndexTable / ConfigurableIndexTable |
Data table with sorting, selection, bulk actions, pagination |
Page / ShopifyPage |
Page wrapper with automatic SkeletonPage loading state |
Toast / ToastNotification |
Toast notifications with ToastProvider context |
ConfirmationModal |
Confirm/cancel modal dialog |
EmptyState / ShopifyEmptyState |
Empty state with illustration and CTA |
SkeletonPage / SkeletonPageLayout |
Skeleton loading layout |
StatusBadge |
Status badge with semantic colour mapping |
Tabs / TabbedNavigation |
Tabbed navigation with content panels |
SearchBar |
Debounced search input |
ResourceList / ConfigurableResourceList |
Resource list with sorting, filtering, bulk actions |
DateRangePicker |
Date range picker with preset ranges and calendar |
BannerNotification |
Dismissible banner with actions |
Pagination / CursorPagination |
Cursor-based pagination controls |
import { IndexTable, Page, Toast, SearchBar } from '@ashishnitw/shopify-utils/components';
function OrdersPage() {
return (
<Page title="Orders" loading={false}>
<SearchBar value={query} onChange={setQuery} placeholder="Search orders..." />
<IndexTable
resourceName={{ singular: 'order', plural: 'orders' }}
items={orders}
headings={[{ title: 'Order' }, { title: 'Customer' }, { title: 'Total' }]}
renderRow={(order) => (
<IndexTable.Row id={order.id}>
<IndexTable.Cell>{order.name}</IndexTable.Cell>
<IndexTable.Cell>{order.customer}</IndexTable.Cell>
<IndexTable.Cell>{order.total}</IndexTable.Cell>
</IndexTable.Row>
)}
/>
</Page>
);
}| Hook | Description |
|---|---|
useFetch |
Fetch with abort, retry, error handling, and optimistic updates |
useAuthenticatedFetch |
useFetch with automatic Shopify session token injection |
usePagination |
Cursor-based pagination state management |
useDebounce / useDebouncedCallback |
Debounce values or callbacks |
useToggle |
Boolean toggle state |
useLocalStorage |
Persistent state backed by localStorage |
useToast |
Toast notification queue management |
useForm |
Form state, validation, dirty tracking, and submission |
useBulkActions |
Multi-select / bulk action state for lists |
useAppBridge |
Shopify App Bridge helpers (redirect, toast, modal, session token) |
import { useFetch, usePagination, useDebounce } from '@ashishnitw/shopify-utils/hooks';
function ProductList() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const pagination = usePagination({ initialPageSize: 20 });
const { data, loading } = useFetch(
`/api/products?q=${debouncedQuery}&after=${pagination.cursors.after}`
);
useEffect(() => {
if (data?.pageInfo) pagination.setCursors(data.pageInfo);
}, [data]);
return (/* render products with pagination controls */);
}const {
shopifyGet, shopifyPost, shopifyPut, shopifyDelete,
shopifyGraphQL,
createShopifyClient, createGraphQLClient,
runBulkQuery, createBulkQuery, pollBulkOperation,
} = require('@ashishnitw/shopify-utils/server/api');
// REST
const products = await shopifyGet(shop, accessToken, 'products.json');
// GraphQL
const result = await shopifyGraphQL(shop, accessToken, `{
products(first: 10) {
edges { node { id title } }
}
}`);
// Bulk operations
const allProducts = await runBulkQuery(shop, accessToken, `{
products { edges { node { id title } } }
}`);const {
verifyShopifyRequest, // HMAC query-param verification
verifyShopifySession, // JWT session token verification
verifyShopifyProxy, // App proxy signature verification
shopifyErrorHandler, // Express error handler
notFoundHandler, // 404 handler
shopifyRateLimiter, // Rate limiting (memory or Redis store)
shopifyCSP, // Content Security Policy for embedded apps
createSessionManager, // MongoDB-backed session storage
} = require('@ashishnitw/shopify-utils/server/middleware');
// Express usage
app.use(shopifyCSP({ apiKey: process.env.SHOPIFY_API_KEY }));
app.use('/api', verifyShopifySession());
app.use('/api', shopifyRateLimiter({ windowMs: 60000, maxRequests: 100 }));
app.use(shopifyErrorHandler());
app.use(notFoundHandler());const {
createWebhookRouter,
createGDPRRouter,
registerWebhooks,
syncWebhooks,
WEBHOOK_TOPICS,
} = require('@ashishnitw/shopify-utils/server/webhooks');
// Mount webhook handler
app.use('/webhooks', createWebhookRouter({
'app/uninstalled': async (shop, payload) => {
await cleanupShopData(shop);
},
'orders/create': async (shop, payload) => {
await processNewOrder(shop, payload);
},
}));
// Mount mandatory GDPR endpoints
app.use('/webhooks/gdpr', createGDPRRouter({
onCustomerDataRequest: async (shop, payload) => { /* ... */ },
onCustomerRedact: async (shop, payload) => { /* ... */ },
onShopRedact: async (shop, payload) => { /* ... */ },
}));
// Register webhooks on app install
await registerWebhooks(shop, accessToken, [
{ topic: 'app/uninstalled', address: 'https://myapp.com/webhooks' },
{ topic: 'orders/create', address: 'https://myapp.com/webhooks' },
]);| Utility | Key Exports |
|---|---|
| Logger | createLogger(namespace), defaultLogger |
| Currency | formatMoney, parseMoney, centsToDollars, dollarsToCents, SHOPIFY_CURRENCIES |
| Dates | formatShopifyDate, parseShopifyDate, timeAgo, getDateRange, daysBetween |
| GraphQL | buildGraphQLQuery, buildMutation, extractNodes, extractPageInfo, paginateQuery |
| Validators | isValidShopDomain, sanitizeShopDomain, isValidShopifyGid, parseShopifyGid, toShopifyGid |
| Constants | SHOPIFY_AUTH_SCOPES, SHOPIFY_ORDER_STATUS, SHOPIFY_PRODUCT_STATUS, SHOPIFY_API_VERSIONS |
| Errors | ShopifyError, ShopifyAuthError, ShopifyRateLimitError, ShopifyNotFoundError, wrapError |
const {
formatMoney, timeAgo, isValidShopDomain,
buildGraphQLQuery, extractNodes,
createLogger, ShopifyAuthError,
} = require('@ashishnitw/shopify-utils/utils');
formatMoney(1999, 'USD'); // "$19.99"
timeAgo(new Date('2024-01-01')); // "1y ago"
isValidShopDomain('my-store.myshopify.com'); // true
const logger = createLogger('my-app');
logger.info('App started', { shop: 'example.myshopify.com' });The server utilities expect these environment variables:
| Variable | Required | Description |
|---|---|---|
SHOPIFY_API_KEY |
Yes | Shopify app API key |
SHOPIFY_API_SECRET |
Yes | Shopify app API secret |
SHOPIFY_HOST_NAME |
No | App hostname (default: localhost) |
SHOPIFY_API_VERSION |
No | API version (default: 2025-04) |
MONGO_URI |
For sessions | MongoDB connection URI |
MONGO_DB_NAME |
No | Database name (default: shopify_app) |
LOG_LEVEL |
No | Log level: error, warn, info, debug, trace (default: info) |
# Authenticate with GitHub Packages
echo "//npm.pkg.github.com/:_authToken=YOUR_TOKEN" >> ~/.npmrc
# Publish
npm publishMIT