Hydra is now in beta|Get started free|Follow our journey on X.com

Next.js
On this page

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 key
  • NEXT_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