Creación de aplicaciones Shopify personalizadas con Remix y Polaris
La migración de Shopify a Remix como marco de aplicación predeterminado marca un cambio fundamental en la forma en que se crean las aplicaciones de producción en la plataforma. La plantilla de aplicación Shopify basada en Remix ofrece renderizado del lado del servidor, transmisión, enrutamiento anidado e integración nativa de Shopify CLI en una única pila coherente, reemplazando los patrones Node/Express + React más antiguos que dominaron el desarrollo de aplicaciones hasta 2023.
Esta guía recorre el ciclo de vida completo del desarrollo: andamiaje, autenticación, obtención de datos GraphQL, integración de componentes Polaris, webhooks, facturación y los patrones que los desarrolladores experimentados de Shopify utilizan para enviar aplicaciones integradas de alto rendimiento y mantenimiento.
Conclusiones clave
- Shopify CLI 3.x genera una aplicación Remix lista para producción con OAuth, almacenamiento de sesiones y App Bridge preconfigurados
- El asistente
authenticate.adminmaneja todo el flujo de OAuth; no implemente OAuth manualmente- El patrón de acción/cargador de Remix se asigna perfectamente al ciclo de mutación/solicitud GraphQL de Shopify.
- Los componentes de Polaris son necesarios para la aprobación de la App Store: la interfaz de usuario personalizada que ignora los estándares de Polaris no pasa la revisión
- Los webhooks deben registrarse a través de la configuración de la aplicación, no solo llamadas API, para mayor confiabilidad.
- La API GraphQL de Shopify Admin está paginada: siempre maneje la paginación basada en cursor en los cargadores de datos
- App Bridge proporciona una experiencia integrada sin Chrome; use el gancho
useAppBridgepara la navegación- Se requiere la integración de la API de facturación para cualquier aplicación que cobre tarifas de suscripción
Configuración y andamiaje del proyecto
Comience con Shopify CLI 3 para crear una aplicación Remix configurada correctamente:
npm install -g @shopify/cli@latest
shopify app create node --template remix
cd your-app-name
El andamio generado incluye:
remix.config.jscon configuración compatible con Shopifyshopify.app.toml— el archivo de configuración de su aplicación (reemplaza.envpara la configuración de la aplicación)app/shopify.server.ts— la autenticación central y la configuración del cliente APIapp/routes/app.tsx: el diseño de la aplicación raíz integradaprisma/schema.prisma— Almacenamiento de sesión SQLite (reemplazar con PostgreSQL para producción)
estructura shopify.app.toml:
name = "your-app-name"
client_id = "your-api-key"
application_url = "https://your-app-url.com"
embedded = true
[access_scopes]
scopes = "read_products,write_products,read_orders"
[auth]
redirect_urls = ["https://your-app-url.com/auth/callback"]
[webhooks]
api_version = "2025-01"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks"
Autenticación con authenticate.admin
El archivo shopify.server.ts exporta un objeto authenticate que maneja todas las cuestiones de autenticación. Nunca implemente OAuth manualmente: el asistente authenticate.admin administra todo el flujo, incluido:
- Intercambio de código OAuth
- Persistencia de la sesión
- Actualización de token
- Modos de acceso en línea versus fuera de línea
- Recopilación de consentimiento del comerciante
Usando autenticación en cargadores de rutas:
// app/routes/app.products.tsx
import { json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin, session } = await authenticate.admin(request);
const response = await admin.graphql(`
#graphql
query GetProducts($first: Int!) {
products(first: $first) {
nodes {
id
title
status
priceRangeV2 {
minVariantPrice {
amount
currencyCode
}
}
totalInventory
}
pageInfo {
hasNextPage
endCursor
}
}
}
`, {
variables: { first: 50 }
});
const data = await response.json();
return json({ products: data.data.products });
};
Sesiones en línea versus fuera de línea:
- Acceso sin conexión: predeterminado para la mayoría de las aplicaciones. La sesión persiste independientemente del inicio de sesión del comerciante. Se utiliza para trabajos en segundo plano, webhooks y tareas programadas.
- Acceso en línea: Sesión vinculada al usuario comerciante que inició sesión. Se requiere cuando necesita actuar en nombre de un usuario específico (por ejemplo, rastrear qué miembro del personal realizó una acción).
Configurar en shopify.server.ts:
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
scopes: process.env.SCOPES!.split(","),
appUrl: process.env.SHOPIFY_APP_URL!,
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
future: {
unstable_newEmbeddedAuthStrategy: true, // Token Exchange (2024+)
},
});
Intercambio de tokens (recomendado para 2026)
La nueva estrategia de autenticación Token Exchange de Shopify elimina las redirecciones OAuth para aplicaciones integradas. Los usuarios dentro del administrador de Shopify no necesitan salir y regresar: el token de sesión de App Bridge se intercambia directamente por un token de acceso API en el lado del servidor. Habilite esto con unstable_newEmbeddedAuthStrategy: true.
Patrones de obtención de datos GraphQL
La API de administración de Shopify es la primera en GraphQL. Domine estos patrones para obtener datos con calidad de producción.
Paginación basada en cursor:
export async function getAllProducts(admin: AdminApiContext) {
let hasNextPage = true;
let cursor: string | null = null;
const allProducts = [];
while (hasNextPage) {
const response = await admin.graphql(`
#graphql
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
nodes {
id
title
handle
}
pageInfo {
hasNextPage
endCursor
}
}
}
`, {
variables: { first: 250, after: cursor }
});
const data = await response.json();
const { nodes, pageInfo } = data.data.products;
allProducts.push(...nodes);
hasNextPage = pageInfo.hasNextPage;
cursor = pageInfo.endCursor;
}
return allProducts;
}
Mutaciones con manejo de errores:
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const title = formData.get("title") as string;
const price = formData.get("price") as string;
const response = await admin.graphql(`
#graphql
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
`, {
variables: {
input: {
title,
variants: [{ price }]
}
}
});
const data = await response.json();
if (data.data.productCreate.userErrors.length > 0) {
return json({
errors: data.data.productCreate.userErrors
}, { status: 422 });
}
return json({ product: data.data.productCreate.product });
};
Operaciones masivas para grandes conjuntos de datos:
Para operaciones en miles de registros, utilice la API de operaciones masivas en lugar de consultas paginadas. Las consultas masivas se ejecutan de forma asíncrona y devuelven un archivo JSONL:
const bulkQuery = await admin.graphql(`
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
title
variants {
edges {
node {
id
price
inventoryQuantity
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
`);
Sondee currentBulkOperation hasta status === "COMPLETED", luego descargue y analice el JSONL desde url.
Integración del sistema de diseño Polaris
Polaris es el sistema de diseño de Shopify para aplicaciones integradas. Se requiere el uso de Polaris para los envíos a la App Store; la interfaz de usuario que no sea Polaris resultará en el rechazo de la revisión.
Configuración de Polaris en Remix:
// app/root.tsx
import { AppProvider } from "@shopify/polaris";
import "@shopify/polaris/build/esm/styles.css";
import translations from "@shopify/polaris/locales/en.json";
export default function App() {
return (
<AppProvider i18n={translations}>
<Outlet />
</AppProvider>
);
}
Patrones de componentes Polaris comunes para aplicaciones de Shopify:
// Product listing with DataTable
import {
Page,
Card,
DataTable,
Button,
Badge,
Filters,
EmptyState
} from "@shopify/polaris";
export default function ProductsPage() {
const { products } = useLoaderData<typeof loader>();
const rows = products.nodes.map(product => [
product.title,
<Badge tone={product.status === 'ACTIVE' ? 'success' : 'warning'}>
{product.status}
</Badge>,
product.totalInventory,
`${product.priceRangeV2.minVariantPrice.currencyCode} ${product.priceRangeV2.minVariantPrice.amount}`,
<Button url={`/app/products/${product.id}`}>Edit</Button>
]);
return (
<Page
title="Products"
primaryAction={{ content: "Add product", url: "/app/products/new" }}
>
<Card>
<DataTable
columnContentTypes={['text', 'text', 'numeric', 'numeric', 'text']}
headings={['Title', 'Status', 'Inventory', 'Price', 'Actions']}
rows={rows}
/>
</Card>
</Page>
);
}
Manejo de formularios con Polaris y Remix:
import { Form, useNavigation, useActionData } from "@remix-run/react";
import { TextField, Select, FormLayout, Banner } from "@shopify/polaris";
export default function ProductForm() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<FormLayout>
{actionData?.errors && (
<Banner tone="critical">
{actionData.errors.map(e => <p key={e.field}>{e.message}</p>)}
</Banner>
)}
<TextField
label="Product title"
name="title"
autoComplete="off"
/>
<TextField
label="Price"
name="price"
type="number"
prefix="$"
autoComplete="off"
/>
<Button submit loading={isSubmitting}>Save product</Button>
</FormLayout>
</Form>
);
}
Webhooks: Registro y Procesamiento
El manejo confiable de webhooks es fundamental para las aplicaciones que reaccionan a los eventos de la tienda comercial.
Registro de webhooks a través de shopify.app.toml:
[[webhooks.subscriptions]]
topics = ["products/create", "products/update", "products/delete"]
uri = "/webhooks"
[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks/orders"
Procesamiento de webhooks en una ruta Remix:
// app/routes/webhooks.tsx
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, session, payload } = await authenticate.webhook(request);
switch (topic) {
case "PRODUCTS_CREATE":
await db.product.create({
data: {
shopifyId: payload.id.toString(),
title: payload.title,
shop: shop,
}
});
break;
case "APP_UNINSTALLED":
if (session) {
await db.session.deleteMany({ where: { shop } });
}
break;
default:
throw new Response("Unhandled webhook topic", { status: 404 });
}
return new Response(null, { status: 200 });
};
Mejores prácticas de confiabilidad del webhook:
- Devuelva una respuesta 200 inmediatamente: procese de forma asincrónica a través de una cola en segundo plano si la operación es lenta
- Implemente la idempotencia utilizando el encabezado
X-Shopify-Webhook-Iddel webhook - Verifique la firma HMAC (manejada por
authenticate.webhook) - Manejar la lógica de reintento: Shopify reintentó webhooks fallidos hasta 19 veces en 48 horas
- Registre todas las cargas útiles de webhooks para su depuración (elimine la PII antes de iniciar sesión)
API de facturación: cargos de suscripción y uso
Cualquier aplicación que cobre tarifas debe utilizar la API de facturación de Shopify. Omitirlo viola las políticas de la App Store.
Compra única de la aplicación:
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing, session } = await authenticate.admin(request);
const { hasActivePayment, appSubscription } = await billing.check({
plans: ["Professional Plan"],
isTest: process.env.NODE_ENV !== "production",
});
if (!hasActivePayment) {
await billing.request({
plan: "Professional Plan",
isTest: process.env.NODE_ENV !== "production",
});
}
return json({ session });
};
Configurar planes de facturación en shopify.server.ts:
const shopify = shopifyApp({
// ...other config
billing: {
"Professional Plan": {
amount: 29.99,
currencyCode: "USD",
interval: BillingInterval.Every30Days,
},
"Enterprise Plan": {
amount: 99.99,
currencyCode: "USD",
interval: BillingInterval.Every30Days,
}
}
});
Puente de aplicaciones y navegación integrada
App Bridge es la biblioteca de JavaScript que administra la experiencia del iframe integrado dentro del administrador de Shopify.
Patrones clave de App Bridge:
import { useAppBridge } from "@shopify/app-bridge-react";
import { Redirect } from "@shopify/app-bridge/actions";
// Navigate to an external URL from embedded context
function ExternalLinkButton() {
const app = useAppBridge();
const handleClick = () => {
const redirect = Redirect.create(app);
redirect.dispatch(Redirect.Action.REMOTE, {
url: "https://your-docs-site.com",
newContext: true
});
};
return <Button onClick={handleClick}>View documentation</Button>;
}
// Toast notifications
import { useToast } from "@shopify/app-bridge-react";
function SaveButton() {
const { show } = useToast();
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.state === "idle" && fetcher.data?.success) {
show("Settings saved");
}
}, [fetcher.state, fetcher.data]);
return (
<Button
onClick={() => fetcher.submit(formData, { method: "post" })}
loading={fetcher.state !== "idle"}
>
Save
</Button>
);
}
Pruebas y desarrollo local
# Start the app in development mode
shopify app dev
# The CLI handles:
# - ngrok tunnel creation for webhook delivery
# - App installation on your development store
# - Hot module replacement for Remix routes
# - Shopify Partner Dashboard app URL updates
Prueba de consultas GraphQL:
Usa la aplicación Shopify GraphiQL en el administrador de tu tienda de desarrollo para probar consultas antes de implementarlas en el código. Acceso vía: your-store.myshopify.com/admin/apps/graphiql.
Cargadores de rutas de prueba unitaria:
import { describe, it, expect, vi } from "vitest";
import { loader } from "~/routes/app.products";
vi.mock("~/shopify.server", () => ({
authenticate: {
admin: vi.fn().mockResolvedValue({
admin: {
graphql: vi.fn().mockResolvedValue({
json: () => Promise.resolve({
data: {
products: {
nodes: [{ id: "gid://shopify/Product/1", title: "Test Product" }],
pageInfo: { hasNextPage: false, endCursor: null }
}
}
})
})
},
session: { shop: "test-shop.myshopify.com" }
})
}
}));
describe("Products loader", () => {
it("returns product list", async () => {
const request = new Request("https://test.com/app/products");
const response = await loader({ request, params: {}, context: {} });
const data = await response.json();
expect(data.products.nodes).toHaveLength(1);
expect(data.products.nodes[0].title).toBe("Test Product");
});
});
Preguntas frecuentes
¿Debo usar la plantilla Remix o la plantilla Node/Express para nuevas aplicaciones de Shopify?
Utilice la plantilla Remix para todas las aplicaciones nuevas. Shopify ha estandarizado Remix para sus herramientas de desarrollo de aplicaciones: la CLI de Shopify, la documentación y App Bridge React están optimizados para Remix. La plantilla Node/Express ya no se desarrolla activamente y carece de varias funciones de autenticación modernas, incluido Token Exchange. Las aplicaciones Node/Express existentes no necesitan migrar inmediatamente, pero todas las aplicaciones nuevas deben comenzar con Remix.
¿Puedo usar un marco de interfaz de usuario diferente en lugar de Polaris?
Puedes usar tu propio sistema de diseño para las partes públicas de tu aplicación (páginas de configuración en tu propio dominio, páginas de destino, etc.), pero las secciones integradas dentro del administrador de Shopify deben usar Polaris. Las pautas de revisión de aplicaciones requieren explícitamente Polaris para la interfaz de usuario integrada. Intentar replicar Polaris visualmente con componentes personalizados generalmente resulta en el rechazo de la revisión debido a inconsistencias de comportamiento y accesibilidad.
¿Cómo manejo los límites de tarifas de la API de Shopify en mi aplicación?
La API de administración GraphQL de Shopify utiliza un modelo de limitación de tasa de "depósito". Cada tienda le otorga a su aplicación 1000 puntos de costo por depósito, que se regenera a 50 puntos por segundo. Supervise el campo extensions.cost.throttleStatus en las respuestas de GraphQL. Para operaciones masivas (que afectan a miles de registros), utilice la API de operaciones masivas, que tiene límites de velocidad separados. Implemente un retroceso exponencial con fluctuación cuando reciba 429 respuestas.
¿Qué base de datos debo usar para mi aplicación Shopify en producción?
Reemplace el SQLite predeterminado con PostgreSQL para producción. El adaptador de almacenamiento de sesiones de Prisma funciona con PostgreSQL: cambie la cadena de conexión en las variables de entorno y actualice prisma/schema.prisma para usar el proveedor postgresql. Para aplicaciones con grandes volúmenes de sesiones, considere Redis para el almacenamiento de sesiones en lugar de PostgreSQL para reducir la carga de la base de datos en la ruta de autenticación activa.
¿Cómo implemento una aplicación de Shopify creada con Remix?
Shopify recomienda implementar en plataformas que admitan el tiempo de ejecución de Node.js: Fly.io, Render, Railway, AWS (EC2/ECS) o Google Cloud Run. Vercel y Netlify funcionan para el frontend pero no pueden ejecutar el servidor Node.js persistente que requiere el almacenamiento de sesiones de Shopify. Asegúrese de que su plataforma de implementación admita procesos de larga duración para el procesamiento de webhooks y trabajos en segundo plano.
Próximos pasos
Crear una aplicación Shopify personalizada que maneje correctamente la autenticación, la administración de datos GraphQL, los webhooks y la facturación requiere una profunda experiencia en la plataforma. Una aplicación mal configurada no pasa la revisión de la App Store, daña las tiendas comerciales o expone vulnerabilidades de seguridad.
Los servicios de desarrollo de aplicaciones Shopify de ECOSIRE cubren el ciclo de vida completo del desarrollo: diseño de arquitectura, implementación de Remix/Polaris, integración de API GraphQL, infraestructura de webhook, configuración de facturación, envío de App Store y soporte posterior al lanzamiento.
Discute los requisitos de tu aplicación Shopify personalizada con nuestro equipo de desarrollo.
Escrito por
ECOSIRE Research and Development Team
Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.
Artículos relacionados
Case Study: eCommerce Migration to Shopify with Odoo Backend
How a fashion retailer migrated from WooCommerce to Shopify and connected it to Odoo ERP, cutting order fulfillment time by 71% and growing revenue 43%.
Integrating GoHighLevel CRM with eCommerce Stores
Step-by-step guide to integrating GoHighLevel CRM with Shopify and WooCommerce. Sync orders, automate post-purchase flows, and recover abandoned carts at scale.
Odoo + Shopify Sync: Products, Orders, and Inventory
Complete guide to syncing Odoo 19 with Shopify. Covers product sync, real-time order import, bidirectional inventory, financial reconciliation, and multi-store management.