@ecomiq/storefront
v1.1.9
Published
SDK headless de storefront Ecomiq: adapters Next.js y TanStack Start sobre React (sin @ecomiq/ui)
Maintainers
Readme
@ecomiq/storefront
SDK headless para construir tu tienda con Next.js o TanStack Start y React sobre la API de Ecomiq.
Incluye tipos, schemas Zod, cliente BFF (/api/store), hooks, EcomiqProvider y bootstrap server por framework. No incluye UI de tienda (CMS, temas, checkout, etc.): la implementas en tu proyecto.
Requisitos
- Node.js 20+
- React 19+
- Next.js 15+ (App Router) o TanStack Start (
@tanstack/react-start) - Tailwind CSS 4+ (opcional; ver
/nextjs/css)
Instalación
npm install @ecomiq/storefront
# o
pnpm add @ecomiq/storefrontInstala también el peer de tu stack:
# Next.js
pnpm add next react react-dom @tanstack/react-query better-auth
# TanStack Start
pnpm add @tanstack/react-start react react-dom @tanstack/react-query better-authNext.js — en next.config:
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@ecomiq/storefront"],
};
export default nextConfig;ecomiq.config.js
En la raíz de tu app (ecomiq.config.js, .cjs o .mjs):
// @ts-check
/** @type {import('@ecomiq/storefront').EcomiqConfig} */
const ecomiqConfig = {
slugStore: "mi-tienda",
};
module.exports = ecomiqConfig;Si defines slugStore, getPublicStore usa ese slug (útil en localhost). Si no, deriva del header Host. Plantilla: ecomiq.config.example.cjs.
Puntos de entrada (npm)
| Import | Uso |
|--------|-----|
| @ecomiq/storefront | Tipos, schemas Zod, utilidades vanilla (formatCurrency, …). Server (react-server): config (loadEcomiqConfig, …) |
| @ecomiq/storefront/react | Client ("use client"): EcomiqProvider, hooks BFF, componentes reutilizables |
| @ecomiq/storefront/nextjs | Server (react-server): getPublicStore, cliente BFF server, auth helpers. Client: reexport de /react |
| @ecomiq/storefront/nextjs/api | Proxy /api/store/* (App Router) |
| @ecomiq/storefront/nextjs/css | Tailwind mínimo (opcional) |
| @ecomiq/storefront/nextjs/postcss | PostCSS de referencia |
| @ecomiq/storefront/tanstack | Client: reexport /react (hooks, provider) |
| @ecomiq/storefront/tanstack/server | Server: getPublicStore, BFF, auth (loaders, createServerFn) |
| @ecomiq/storefront/tanstack/api | Proxy /api/store/* (server routes) |
Importa hooks y EcomiqProvider desde @ecomiq/storefront/react. El build npm separa server (react-server), vanilla (raíz) y React (/react) para App Router.
Los hooks (useStoreProducts, useStoreCart, …) viven en /react; /nextjs y /tanstack los reexportan en client por compatibilidad.
Peers
| Paquete | Cuándo |
|---------|--------|
| react, react-dom | Siempre |
| next | Solo si usas /nextjs |
| @tanstack/react-start | Solo si usas /tanstack |
Cloudflare: el SDK no incluye @opennextjs/cloudflare. Si despliegas en Cloudflare con settings vía KV (PUBLIC_STORE_SETTINGS_SOURCE=kv), instálalo y configura wrangler en tu proyecto.
Arquitectura
┌─────────────────────────────┐
│ @ecomiq/storefront │ ecomiq.config.js
└──────────────┬──────────────┘
│
┌─────────────────────────┴─────────────────────────┐
▼ ▼
/nextjs (+ /nextjs/api) /tanstack (+ /tanstack/api)
getPublicStore · toNextJsStoreApiHandler getPublicStore · createTanStackStoreApiHandler
│ │
└─────────────────────────┬─────────────────────────┘
▼
Capa React (`/react`) — hooks, EcomiqProvider, cliente BFF browser, auth
│
▼
core/server — fetchPublicStoreSettings · handleStoreProxyRequest
│
▼
Navegador → /api/store/* (proxy en tu app) → API Ecomiq
↑ contexto X-Store-Id (tras getPublicStore)El browser no llama al upstream directo; montas el proxy con el adapter de tu framework.
Inicio rápido — Next.js
1. Proxy BFF
// app/api/store/[...path]/route.ts
import { toNextJsStoreApiHandler } from "@ecomiq/storefront/nextjs/api";
export const { GET, POST, PUT, PATCH, DELETE } = toNextJsStoreApiHandler({
apiBaseUrl: process.env.STOREFRONT_API_URL!,
});Los hooks y EcomiqProvider son Client Components ("use client"). En el layout (Server Component) solo importes funciones server; el provider va en un archivo client aparte.
2. Layout y tienda
// app/ecomiq-provider.tsx
"use client";
import { EcomiqProvider } from "@ecomiq/storefront/react";
import type { StorefrontSettings } from "@ecomiq/storefront";
export function EcomiqRootProvider({
store,
children,
}: {
store: StorefrontSettings | null;
children: React.ReactNode;
}) {
return <EcomiqProvider store={store}>{children}</EcomiqProvider>;
}// app/layout.tsx — Server Component
import { getPublicStore } from "@ecomiq/storefront/nextjs";
import { EcomiqRootProvider } from "./ecomiq-provider";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { data: store, error } = await getPublicStore();
if (error) {
// tu página de error / tienda no encontrada
}
return (
<html lang="es">
<body>
<EcomiqRootProvider store={store}>{children}</EcomiqRootProvider>
</body>
</html>
);
}getPublicStore corre en el servidor. EcomiqProvider hidrata useStore, configura React Query y el storeId del cliente BFF — debe vivir en un módulo con "use client".
3. Catálogo con hooks
Importa hooks desde @ecomiq/storefront/react:
"use client";
import { useStoreProducts } from "@ecomiq/storefront/react";
export function ProductList() {
const { useStorefrontProductsList } = useStoreProducts();
const { data, isLoading } = useStorefrontProductsList({ page: 1, pageSize: 24 });
if (isLoading) return <p>Cargando…</p>;
return (
<ul>
{data?.products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}4. Datos en Server Components
import { getStorefrontProducts } from "@ecomiq/storefront/nextjs";
export default async function CatalogPage() {
const { products } = await getStorefrontProducts({ page: 1, pageSize: 24 });
return <YourGrid products={products} />;
}Inicio rápido — TanStack Start
1. Proxy BFF
Ruta splat: /api/store/* → routes/api/store/$.ts
// src/routes/api/store/$.ts
import { createFileRoute } from "@tanstack/react-router";
import { createTanStackStoreApiHandler } from "@ecomiq/storefront/tanstack/api";
const storeApi = createTanStackStoreApiHandler({
apiBaseUrl: process.env.STOREFRONT_API_URL,
});
export const Route = createFileRoute("/api/store/$")({
server: { handlers: storeApi },
});2. Tienda en root route
// src/components/ecomiq-provider.tsx
"use client";
import { EcomiqProvider } from "@ecomiq/storefront/react";
import type { StorefrontSettings } from "@ecomiq/storefront/tanstack";
export function EcomiqRootProvider({
store,
children,
}: {
store: StorefrontSettings | null;
children: React.ReactNode;
}) {
return <EcomiqProvider store={store}>{children}</EcomiqProvider>;
}// src/routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { getPublicStore } from "@ecomiq/storefront/tanstack/server";
import { EcomiqRootProvider } from "../components/ecomiq-provider";
export const Route = createRootRoute({
loader: async () => getPublicStore(),
component: RootComponent,
});
function RootComponent() {
const { data: store } = Route.useLoaderData();
return (
<EcomiqRootProvider store={store}>
<Outlet />
</EcomiqRootProvider>
);
}3. Hooks (Client Components)
"use client";
import { useStoreProducts, useStoreCart } from "@ecomiq/storefront/react";Autenticación (Better Auth + SDK)
Instala Better Auth.
Servidor:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { ecomiqOAuthPassword } from "@ecomiq/storefront/nextjs";
// TanStack: import { ecomiqOAuthPassword } from "@ecomiq/storefront/tanstack";
export const auth = betterAuth({
plugins: [ecomiqOAuthPassword()],
});Ruta API (Next.js):
// app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler(auth);Cliente:
// lib/auth-client.ts
"use client";
import { createAuthClient } from "better-auth/react";
import { ecomiqOAuthPasswordClient } from "@ecomiq/storefront/nextjs";
export const authClient = createAuthClient({
plugins: [ecomiqOAuthPasswordClient()],
});Layout — pasa authClient al provider:
<EcomiqProvider store={store} authClient={authClient}>
{children}
</EcomiqProvider>Login, registro y logout
"use client";
import {
useStore,
useStorefrontAuthClient,
useStorefrontSession,
registerCustomer,
signInWithEcomiqCredentials,
} from "@ecomiq/storefront/nextjs";
export function AccountActions() {
const { data: store } = useStore();
const authClient = useStorefrontAuthClient();
const { isAuthenticated, user, signOut, refetch } = useStorefrontSession();
async function handleLogin(email: string, password: string) {
await signInWithEcomiqCredentials(authClient, store, { email, password });
await refetch();
}
async function handleRegister(input: {
email: string;
password: string;
firstName: string;
lastName: string;
countryIsoCode: string;
phone: string;
}) {
await registerCustomer(input);
await handleLogin(input.email, input.password);
}
if (isAuthenticated) {
return (
<div>
<p>Hola, {user?.firstName ?? user?.email}</p>
<button type="button" onClick={() => signOut()}>Cerrar sesión</button>
</div>
);
}
return <div>{/* Tu UI */}</div>;
}En TanStack, cambia el import server a @ecomiq/storefront/tanstack y los hooks a @ecomiq/storefront/react.
Qué incluye cada entry
Desde /nextjs o /tanstack (server, condición react-server):
getPublicStore,getHostname,fetchPublicStoreSettings- Cliente BFF server:
getStorefrontProducts,registerCustomer, carrito/cuenta, etc. - Auth server:
ecomiqOAuthPassword
Desde @ecomiq/storefront/react (client, "use client"):
- Provider:
EcomiqProvider,useStore,usePublicStore - Hooks BFF:
useStoreProducts,useStoreCollections,useStorePages,useStoreCart - Auth client:
ecomiqOAuthPasswordClient,signInWithEcomiqCredentials,useStorefrontSession - Componentes: selector de variantes de producto (más en futuras versiones)
Desde @ecomiq/storefront (raíz — vanilla, server o client):
- Tipos y Zod:
StorefrontSettings,StorefrontProduct, schemas - Utilidades:
formatCurrency,countryCurrency,sanitizeHtml,stripHtml, labels de pedidos,findStorefrontCmsPageBySlug - Config (solo server):
loadEcomiqConfig,EcomiqConfigSchema, …
Variables de entorno
| Variable | Descripción |
|----------|-------------|
| STOREFRONT_API_URL | URL base de la API de tienda (proxy) |
| PUBLIC_STORE_SETTINGS_SOURCE | api (default) o kv (Cloudflare KV; requiere @opennextjs/cloudflare en tu proyecto, no en el SDK) |
Identificador de tienda: slugStore en ecomiq.config.* si existe; si no, header Host.
Dependencias recomendadas
Next.js:
{
"dependencies": {
"@ecomiq/storefront": "^0.1.0",
"next": "^15.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}TanStack Start:
{
"dependencies": {
"@ecomiq/storefront": "^0.1.0",
"@tanstack/react-start": "^1.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}Soporte
Documentación y credenciales: ecomiq.pe (o el portal de tu organización).
