Next.js + Hydra
Build a production storefront using Next.js App Router and the Hydra SDK. This guide covers server-side data fetching with React Server Components, client-side cart management, and deployment to Vercel.
Prerequisites
- Node.js 18+
- A Hydra project with API keys (get them from your admin panel)
1. Create your project
npx create-next-app@latest my-store --typescript --app
cd my-store
npm install @gethydra/sdk
2. Environment variables
Create a .env.local file in your project root:
# Server-side only — never exposed to the browser
HYDRA_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Client-side — prefixed with NEXT_PUBLIC_ so it's available in the browser
NEXT_PUBLIC_HYDRA_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Secret keys (sk_live_*) must only be used on the server. Publishable keys (pk_live_*) are safe for client-side code — they only allow read access and cart operations.
3. SDK setup
Create a server-side SDK instance. This file should only be imported in Server Components and Route Handlers.
// lib/hydra.ts
import { Hydra } from '@gethydra/sdk';
export const hydra = new Hydra({
secret_key: process.env.HYDRA_SECRET_KEY!,
base_url: 'https://api.hydrajs.dev',
});
For client-side operations (cart, checkout), create a separate instance:
// lib/hydra-client.ts
import { Hydra } from '@gethydra/sdk';
export const hydraClient = new Hydra({
publishable_key: process.env.NEXT_PUBLIC_HYDRA_PUBLISHABLE_KEY!,
base_url: 'https://api.hydrajs.dev',
});
4. Product listing page
Fetch products in a React Server Component — no client-side JavaScript needed for the initial render.
// app/products/page.tsx
import { hydra } from '@/lib/hydra';
import Link from 'next/link';
export default async function ProductsPage() {
const { data: products } = await hydra.products.list({ limit: 20 });
return (
<main>
<h1>Products</h1>
<div className="grid grid-cols-3 gap-6">
{products.map((product) => (
<Link key={product.id} href={`/products/${product.handle}`}>
<article>
{product.images[0] && (
<img
src={product.images[0].src}
alt={product.images[0].alt ?? product.title}
width={product.images[0].width}
height={product.images[0].height}
/>
)}
<h2>{product.title}</h2>
<p>
${(product.variants[0].price / 100).toFixed(2)}
</p>
</article>
</Link>
))}
</div>
</main>
);
}
5. Product detail page
Use dynamic routes with params to load a single product by handle.
// app/products/[handle]/page.tsx
import { hydra } from '@/lib/hydra';
import { notFound } from 'next/navigation';
import { AddToCartButton } from './add-to-cart-button';
interface Props {
params: Promise<{ handle: string }>;
}
export default async function ProductPage({ params }: Props) {
const { handle } = await params;
try {
const { data: product } = await hydra.products.getByHandle(handle, {
expand: 'variants,images',
});
return (
<main>
<h1>{product.title}</h1>
<p dangerouslySetInnerHTML={{ __html: product.body_html ?? '' }} />
<div>
{product.variants.map((variant) => (
<div key={variant.id}>
<span>{variant.title}</span>
<span>${(variant.price / 100).toFixed(2)}</span>
<AddToCartButton variantId={variant.id} />
</div>
))}
</div>
</main>
);
} catch {
notFound();
}
}
6. Cart integration
Cart operations run on the client using the publishable key.
// app/products/[handle]/add-to-cart-button.tsx
'use client';
import { hydraClient } from '@/lib/hydra-client';
import { useState } from 'react';
function getCartId(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('hydra_cart_id');
}
function setCartId(id: string) {
localStorage.setItem('hydra_cart_id', id);
}
export function AddToCartButton({ variantId }: { variantId: string }) {
const [loading, setLoading] = useState(false);
async function addToCart() {
setLoading(true);
try {
let cartId = getCartId();
if (!cartId) {
const { data: cart } = await hydraClient.cart.create({
items: [{ variant_id: variantId, quantity: 1 }],
});
setCartId(cart.id);
} else {
await hydraClient.cart.addItem(cartId, {
variant_id: variantId,
quantity: 1,
});
}
} catch (error) {
console.error('Failed to add to cart:', error);
} finally {
setLoading(false);
}
}
return (
<button onClick={addToCart} disabled={loading}>
{loading ? 'Adding...' : 'Add to cart'}
</button>
);
}
7. Cart page
Display the cart and provide a checkout button.
// app/cart/page.tsx
'use client';
import { hydraClient } from '@/lib/hydra-client';
import { useEffect, useState } from 'react';
interface CartItem {
id: string;
variant_id: string;
title: string;
quantity: number;
price: number;
}
interface Cart {
id: string;
items: CartItem[];
total: number;
}
export default function CartPage() {
const [cart, setCart] = useState<Cart | null>(null);
useEffect(() => {
const cartId = localStorage.getItem('hydra_cart_id');
if (cartId) {
hydraClient.cart.get(cartId).then(({ data }) => setCart(data));
}
}, []);
if (!cart || cart.items.length === 0) {
return <p>Your cart is empty.</p>;
}
return (
<main>
<h1>Cart</h1>
<ul>
{cart.items.map((item) => (
<li key={item.id}>
{item.title} x {item.quantity} — ${(item.price / 100).toFixed(2)}
</li>
))}
</ul>
<p>Total: ${(cart.total / 100).toFixed(2)}</p>
<CheckoutButton cartId={cart.id} />
</main>
);
}
function CheckoutButton({ cartId }: { cartId: string }) {
const [loading, setLoading] = useState(false);
async function handleCheckout() {
setLoading(true);
const { data: checkout } = await hydraClient.checkout.create({
cart_id: cartId,
});
window.location.href = checkout.checkout_url;
}
return (
<button onClick={handleCheckout} disabled={loading}>
{loading ? 'Redirecting...' : 'Checkout'}
</button>
);
}
8. Deploy to Vercel
npm install -g vercel
vercel
Set your environment variables in the Vercel dashboard under Settings > Environment Variables:
HYDRA_SECRET_KEY— your secret keyNEXT_PUBLIC_HYDRA_PUBLISHABLE_KEY— your publishable key
Vercel automatically detects Next.js projects and configures the build settings. No additional configuration is required.
Next steps
- SDK reference — full list of available methods
- Authentication — API key types and usage
- Pagination — cursor-based pagination for large catalogs