Building Custom Shopify Apps with Remix and Polaris

Complete developer guide to building custom Shopify apps using Remix, Shopify CLI 3, Polaris design system, and App Bridge for embedded admin experiences.

E
ECOSIRE Research and Development Team
|19 mars 202612 min de lecture2.6k Mots|

Création d'applications Shopify personnalisées avec Remix et Polaris

La migration de Shopify vers Remix comme framework d'application par défaut marque un changement fondamental dans la façon dont les applications de production sont créées sur la plateforme. Le modèle d'application Shopify basé sur Remix offre un rendu côté serveur, un streaming, un routage imbriqué et une intégration native de Shopify CLI dans une seule pile cohérente, remplaçant les anciens modèles Node/Express + React qui dominaient le développement d'applications jusqu'en 2023.

Ce guide parcourt le cycle de vie complet du développement : échafaudage, authentification, récupération de données GraphQL, intégration de composants Polaris, webhooks, facturation et modèles expérimentés que les développeurs Shopify utilisent pour fournir des applications intégrées maintenables et performantes.

Points clés à retenir

  • Shopify CLI 3.x génère une application Remix prête pour la production avec OAuth, stockage de session et App Bridge préconfigurés
  • L'assistant authenticate.admin gère l'intégralité du flux OAuth — n'implémentez pas OAuth manuellement
  • Le modèle de chargeur/action de Remix correspond parfaitement au cycle de requête/mutation GraphQL de Shopify
  • Les composants Polaris sont requis pour l'approbation de l'App Store – une interface utilisateur personnalisée qui ignore les normes Polaris échoue à l'examen
  • Les Webhooks doivent être enregistrés via la configuration de l'application, et pas seulement les appels API, pour des raisons de fiabilité
  • L'API Shopify Admin GraphQL est paginée — gérez toujours la pagination basée sur le curseur dans les chargeurs de données
  • App Bridge offre une expérience intégrée sans chrome ; utilisez le crochet useAppBridge pour la navigation
  • L'intégration de l'API de facturation est requise pour toute application facturant des frais d'abonnement

Configuration du projet et échafaudage

Commencez avec Shopify CLI 3 pour créer une application Remix correctement configurée :

npm install -g @shopify/cli@latest
shopify app create node --template remix
cd your-app-name

L'échafaudage généré comprend :

  • remix.config.js avec paramètres compatibles Shopify
  • shopify.app.toml — le fichier de configuration de votre application (remplace .env pour les paramètres de l'application)
  • app/shopify.server.ts — l'authentification centrale et la configuration du client API
  • app/routes/app.tsx — la présentation de l'application racine intégrée
  • prisma/schema.prisma — Stockage de session SQLite (remplacer par PostgreSQL pour la production)

shopify.app.tomlstructure :

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"

Authentification avec authenticate.admin

Le fichier shopify.server.ts exporte un objet authenticate qui gère tous les problèmes d'authentification. N'implémentez jamais OAuth manuellement : l'assistant authenticate.admin gère l'intégralité du flux, notamment :

  • Échange de codes OAuth
  • Persistance de session
  • Actualisation du jeton
  • Modes d'accès en ligne et hors ligne
  • Collecte du consentement du commerçant

Utilisation de l'authentification dans les chargeurs de route :

// 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 });
};

Sessions en ligne et hors ligne :

  • Accès hors ligne : valeur par défaut pour la plupart des applications. La session persiste indépendamment de la connexion du commerçant. Utilisé pour les tâches en arrière-plan, les webhooks et les tâches planifiées.
  • Accès en ligne : Session liée à l'utilisateur marchand connecté. Obligatoire lorsque vous devez agir au nom d'un utilisateur spécifique (par exemple, pour savoir quel membre du personnel a effectué une action).

Configurer dans 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+)
  },
});

Échange de jetons (recommandé pour 2026)

La nouvelle stratégie d'authentification Token Exchange de Shopify élimine les redirections OAuth pour les applications intégrées. Les utilisateurs de l'administrateur Shopify n'ont pas besoin de quitter et de revenir : le jeton de session d'App Bridge est échangé directement contre un jeton d'accès API côté serveur. Activez-le avec unstable_newEmbeddedAuthStrategy: true.


Modèles de récupération de données GraphQL

L'API d'administration de Shopify est d'abord GraphQL. Maîtrisez ces modèles pour une récupération de données de qualité production.

Papagation basée sur le curseur :

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;
}

Mutations avec gestion des erreurs :

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 });
};

Opérations groupées pour les grands ensembles de données :

Pour les opérations sur des milliers d’enregistrements, utilisez l’API Bulk Operations plutôt que les requêtes paginées. Les requêtes groupées s'exécutent de manière asynchrone et renvoient un fichier 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
      }
    }
  }
`);

Interrogez currentBulkOperation jusqu'à status === "COMPLETED", puis téléchargez et analysez le JSONL à partir de url.


Intégration du système de conception Polaris

Polaris est le système de conception de Shopify pour les applications intégrées. L’utilisation de Polaris est requise pour les soumissions sur l’App Store – une interface utilisateur non Polaris entraînera le rejet de l’avis.

Configuration de Polaris dans 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>
  );
}

Modèles de composants Polaris courants pour les applications 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>
  );
}

Gestion des formulaires avec Polaris et 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 : inscription et traitement

Une gestion fiable des webhooks est essentielle pour les applications qui réagissent aux événements des magasins marchands.

Enregistrement des webhooks via shopify.app.toml :

[[webhooks.subscriptions]]
topics = ["products/create", "products/update", "products/delete"]
uri = "/webhooks"

[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks/orders"

Traitement des webhooks dans une route 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 });
};

Bonnes pratiques en matière de fiabilité des webhooks :

  1. Renvoyez immédiatement une réponse 200 – traitez de manière asynchrone via une file d'attente en arrière-plan si l'opération est lente
  2. Implémentez l'idempotence à l'aide de l'en-tête X-Shopify-Webhook-Id du webhook
  3. Vérifiez la signature HMAC (gérée par authenticate.webhook)
  4. Gérer la logique de nouvelle tentative – Shopify réessaye les webhooks ayant échoué jusqu'à 19 fois en 48 heures
  5. Enregistrez toutes les charges utiles du webhook pour le débogage (supprimez les PII avant la journalisation)

API de facturation : frais d'abonnement et d'utilisation

Tous les frais de facturation d'application doivent utiliser l'API de facturation de Shopify. Le contourner viole les politiques de l’App Store.

Achat unique de l'application :

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 });
};

Configurez les forfaits de facturation dans 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,
    }
  }
});

App Bridge et navigation intégrée

App Bridge est la bibliothèque JavaScript qui gère l'expérience iframe intégrée dans l'administrateur Shopify.

Modèles clés d'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>
  );
}

Tests et développement 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

Test des requêtes GraphQL :

Utilisez l'application Shopify GraphiQL dans l'administrateur de votre boutique de développement pour tester les requêtes avant de les implémenter dans le code. Accès via : your-store.myshopify.com/admin/apps/graphiql.

Chargeurs de routes de tests unitaires :

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");
  });
});

Questions fréquemment posées

Dois-je utiliser le modèle Remix ou le modèle Node/Express pour les nouvelles applications Shopify ?

Utilisez le modèle Remix pour toutes les nouvelles applications. Shopify a standardisé Remix pour ses outils de développement d'applications : la CLI, la documentation et App Bridge React de Shopify sont tous optimisés pour Remix. Le modèle Node/Express n'est plus activement développé et ne dispose pas de plusieurs fonctionnalités d'authentification modernes, notamment Token Exchange. Les applications Node/Express existantes n'ont pas besoin d'être migrées immédiatement, mais toutes les nouvelles applications doivent démarrer avec Remix.

Puis-je utiliser un autre framework frontend au lieu de Polaris ?

Vous pouvez utiliser votre propre système de conception pour les parties publiques de votre application (pages de paramètres sur votre propre domaine, pages de destination, etc.), mais les sections intégrées dans l'administrateur Shopify doivent utiliser Polaris. Les directives d'examen des applications nécessitent explicitement Polaris pour l'interface utilisateur intégrée. Tenter de reproduire visuellement Polaris avec des composants personnalisés entraîne généralement un rejet de l'avis en raison d'incohérences de comportement et d'accessibilité.

Comment gérer les limites de débit de l'API Shopify dans mon application ?

L'API GraphQL Admin de Shopify utilise un modèle de limitation de débit « seau ». Chaque boutique donne à votre application 1 000 points de coût par compartiment, qui se régénère à 50 points par seconde. Surveillez le champ extensions.cost.throttleStatus dans les réponses GraphQL. Pour les opérations groupées (affectant des milliers d’enregistrements), utilisez l’API Bulk Operations, qui a des limites de débit distinctes. Implémentez un recul exponentiel avec instabilité lorsque vous recevez 429 réponses.

Quelle base de données dois-je utiliser pour mon application Shopify en production ?

Remplacez le SQLite par défaut par PostgreSQL pour la production. L'adaptateur de stockage de session Prisma fonctionne avec PostgreSQL : modifiez la chaîne de connexion dans vos variables d'environnement et mettez à jour prisma/schema.prisma pour utiliser le fournisseur postgresql. Pour les applications avec des volumes de sessions élevés, envisagez Redis pour le stockage de sessions au lieu de PostgreSQL afin de réduire la charge de la base de données sur le chemin d'authentification à chaud.

Comment déployer une application Shopify créée avec Remix ?

Shopify recommande de déployer sur des plates-formes prenant en charge le runtime Node.js : Fly.io, Render, Railway, AWS (EC2/ECS) ou Google Cloud Run. Vercel et Netlify fonctionnent pour le frontend mais ne peuvent pas exécuter le serveur Node.js persistant requis par le stockage de session de Shopify. Assurez-vous que votre plate-forme de déploiement prend en charge les processus de longue durée pour le traitement des webhooks et les tâches en arrière-plan.


Prochaines étapes

Créer une application Shopify personnalisée qui gère correctement l'authentification, la gestion des données GraphQL, les webhooks et la facturation nécessite une expertise approfondie de la plate-forme. Une application mal configurée échoue à l’examen de l’App Store, interrompt les magasins marchands ou expose des vulnérabilités de sécurité.

Les services de développement d'applications Shopify d'ECOSIRE couvrent le cycle de vie complet du développement : conception de l'architecture, mise en œuvre de Remix/Polaris, intégration de l'API GraphQL, infrastructure de webhook, configuration de la facturation, soumission de l'App Store et assistance post-lancement.

Discutez des exigences de votre application Shopify personnalisée avec notre équipe de développement.

E

Rédigé par

ECOSIRE Research and Development Team

Création de produits numériques de niveau entreprise chez ECOSIRE. Partage d'analyses sur les intégrations Odoo, l'automatisation e-commerce et les solutions d'entreprise propulsées par l'IA.

Discutez sur WhatsApp