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. März 202610 Min. Lesezeit2.2k Wörter|

Erstellen benutzerdefinierter Shopify-Apps mit Remix und Polaris

Die Migration von Shopify zu Remix als Standard-App-Framework markiert einen grundlegenden Wandel in der Art und Weise, wie Produktions-Apps auf der Plattform erstellt werden. Die Remix-basierte Shopify-App-Vorlage bietet serverseitiges Rendering, Streaming, verschachteltes Routing und native Shopify-CLI-Integration in einem einzigen kohärenten Stack und ersetzt damit die älteren Node/Express + React-Muster, die bis 2023 die App-Entwicklung dominierten.

Dieser Leitfaden führt durch den gesamten Entwicklungslebenszyklus: Gerüstbau, Authentifizierung, GraphQL-Datenabruf, Polaris-Komponentenintegration, Webhooks, Abrechnung und die Muster, die erfahrene Shopify-Entwickler verwenden, um wartbare, leistungsstarke eingebettete Apps bereitzustellen.

Wichtige Erkenntnisse

– Shopify CLI 3.x generiert eine produktionsbereite Remix-App mit vorkonfiguriertem OAuth, Sitzungsspeicher und App Bridge – Der authenticate.admin-Helfer verarbeitet den gesamten OAuth-Fluss – implementieren Sie OAuth nicht manuell – Das Loader-/Aktionsmuster von Remix passt perfekt zum GraphQL-Anfrage-/Mutationszyklus von Shopify

  • Polaris-Komponenten sind für die App Store-Genehmigung erforderlich – eine benutzerdefinierte Benutzeroberfläche, die Polaris-Standards ignoriert, besteht bei der Prüfung nicht – Aus Gründen der Zuverlässigkeit müssen Webhooks über die App-Konfiguration und nicht nur über API-Aufrufe registriert werden – Die Shopify Admin GraphQL API ist paginiert – behandeln Sie in Datenladern immer die Cursor-basierte Paginierung
  • App Bridge bietet ein eingebettetes Erlebnis ohne Chrome; Verwenden Sie den Hook useAppBridge für die Navigation – Die Integration der Abrechnungs-API ist für die Erhebung von Abonnementgebühren für Apps erforderlich

Projektaufbau und Gerüstbau

Beginnen Sie mit Shopify CLI 3, um eine ordnungsgemäß konfigurierte Remix-App zu erstellen:

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

Das generierte Gerüst umfasst:

  • remix.config.js mit Shopify-kompatiblen Einstellungen
  • shopify.app.toml – Ihre App-Konfigurationsdatei (ersetzt .env für App-Einstellungen)
  • app/shopify.server.ts – die zentrale Authentifizierung und API-Client-Konfiguration
  • app/routes/app.tsx – das Root-Layout der eingebetteten App
  • prisma/schema.prisma – SQLite-Sitzungsspeicher (für die Produktion durch PostgreSQL ersetzen)

shopify.app.toml-Struktur:

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"

Authentifizierung mit authenticate.admin

Die Datei shopify.server.ts exportiert ein authenticate-Objekt, das alle Authentifizierungsprobleme behandelt. Implementieren Sie OAuth niemals manuell – der authenticate.admin-Helfer verwaltet den gesamten Ablauf, einschließlich:

  • OAuth-Code-Austausch
  • Sitzungspersistenz
  • Token-Aktualisierung
  • Online- und Offline-Zugriffsmodi
  • Einholung der Händlereinwilligung

Authentifizierung in Routenladern verwenden:

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

Online- vs. Offline-Sitzungen:

  • Offline-Zugriff: Standard für die meisten Apps. Die Sitzung bleibt unabhängig von der Händleranmeldung bestehen. Wird für Hintergrundjobs, Webhooks und geplante Aufgaben verwendet.
  • Online-Zugriff: Sitzung, die an den angemeldeten Händlerbenutzer gebunden ist. Erforderlich, wenn Sie im Namen eines bestimmten Benutzers handeln müssen (z. B. nachverfolgen, welcher Mitarbeiter eine Aktion ausgeführt hat).

Konfigurieren Sie in 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+)
  },
});

Token Exchange (empfohlen für 2026)

Die neuere Token Exchange-Authentifizierungsstrategie von Shopify eliminiert OAuth-Weiterleitungen für eingebettete Apps. Benutzer innerhalb des Shopify-Administrators müssen nicht gehen und zurückkehren – das Sitzungstoken von App Bridge wird direkt serverseitig gegen ein API-Zugriffstoken ausgetauscht. Aktivieren Sie dies mit unstable_newEmbeddedAuthStrategy: true.


GraphQL-Datenabrufmuster

Die Admin-API von Shopify ist GraphQL-first. Beherrschen Sie diese Muster für den Datenabruf in Produktionsqualität.

Cursorbasierte Paginierung:

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

Mutationen mit Fehlerbehandlung:

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

Massenoperationen für große Datensätze:

Für Vorgänge an Tausenden von Datensätzen verwenden Sie die Bulk Operations API anstelle von paginierten Abfragen. Massenabfragen werden asynchron ausgeführt und geben eine JSONL-Datei zurück:

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

Fragen Sie currentBulkOperation bis status === "COMPLETED" ab, laden Sie dann JSONL von url herunter und analysieren Sie es.


Polaris Design Systemintegration

Polaris ist Shopifys Designsystem für eingebettete Apps. Für App Store-Einreichungen ist die Verwendung von Polaris erforderlich. Eine Nicht-Polaris-Benutzeroberfläche führt zur Ablehnung der Bewertung.

Polaris in Remix einrichten:

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

Gemeinsame Polaris-Komponentenmuster für Shopify-Apps:

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

Formularbearbeitung mit Polaris und 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: Registrierung und Verarbeitung

Eine zuverlässige Webhook-Verarbeitung ist für Apps, die auf Händler-Store-Ereignisse reagieren, von entscheidender Bedeutung.

Registrieren von Webhooks über shopify.app.toml:

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

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

Verarbeitung von Webhooks in einer Remix-Route:

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

Best Practices für die Webhook-Zuverlässigkeit:

  1. Geben Sie sofort eine 200-Antwort zurück – asynchrone Verarbeitung über eine Hintergrundwarteschlange, wenn der Vorgang langsam ist
  2. Implementieren Sie Idempotenz mithilfe des X-Shopify-Webhook-Id-Headers des Webhooks
  3. Überprüfen Sie die HMAC-Signatur (verarbeitet von authenticate.webhook).
  4. Behandeln Sie die Wiederholungslogik – Shopify wiederholt fehlgeschlagene Webhooks bis zu 19 Mal innerhalb von 48 Stunden
  5. Alle Webhook-Payloads zum Debuggen protokollieren (PII vor der Protokollierung entfernen)

Abrechnungs-API: Abonnement- und Nutzungsgebühren

Alle App-Ladegebühren müssen über die Abrechnungs-API von Shopify erfolgen. Das Umgehen verstößt gegen die App Store-Richtlinien.

Einmaliger App-Kauf:

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

Konfigurieren Sie Abrechnungspläne in 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 und eingebettete Navigation

App Bridge ist die JavaScript-Bibliothek, die das eingebettete Iframe-Erlebnis im Shopify-Adminbereich verwaltet.

Wichtige App Bridge-Muster:

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 und lokale Entwicklung

# 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

Testen von GraphQL-Abfragen:

Verwenden Sie die Shopify GraphiQL-App in Ihrem Entwicklungsshop-Administrator, um Abfragen zu testen, bevor Sie sie in Code implementieren. Zugriff über: your-store.myshopify.com/admin/apps/graphiql.

Komponententest-Routenlader:

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

Häufig gestellte Fragen

Soll ich die Remix-Vorlage oder die Node/Express-Vorlage für neue Shopify-Apps verwenden?

Verwenden Sie die Remix-Vorlage für alle neuen Apps. Shopify hat sich bei seinen App-Entwicklungstools auf Remix geeinigt – die Shopify-CLI, die Dokumentation und App Bridge React sind alle für Remix optimiert. Die Node/Express-Vorlage wird nicht mehr aktiv entwickelt und es fehlen mehrere moderne Authentifizierungsfunktionen, einschließlich Token Exchange. Bestehende Node/Express-Apps müssen nicht sofort migriert werden, aber alle neuen Apps sollten mit Remix starten.

Kann ich anstelle von Polaris ein anderes Frontend-Framework verwenden?

Sie können Ihr eigenes Designsystem für die öffentlich zugänglichen Teile Ihrer App verwenden (Einstellungsseiten auf Ihrer eigenen Domain, Landingpages usw.), eingebettete Abschnitte im Shopify-Adminbereich müssen jedoch Polaris verwenden. Richtlinien zur App-Überprüfung erfordern ausdrücklich, dass Polaris für die eingebettete Benutzeroberfläche vorgesehen ist. Der Versuch, Polaris mit benutzerdefinierten Komponenten visuell nachzubilden, führt in der Regel zu einer Ablehnung der Rezension aufgrund von Verhaltens- und Zugänglichkeitsinkonsistenzen.

Wie gehe ich mit den Ratenbeschränkungen der Shopify-API in meiner App um?

Die GraphQL-Admin-API von Shopify verwendet ein „Bucket“-Ratenbegrenzungsmodell. Jeder Shop stellt Ihrer App 1.000 Kostenpunkte pro Bucket zur Verfügung, die sich mit 50 Punkten pro Sekunde regeneriert. Überwachen Sie das Feld extensions.cost.throttleStatus in GraphQL-Antworten. Für Massenvorgänge (die Tausende von Datensätzen betreffen) verwenden Sie die Bulk Operations API, die über separate Ratenbeschränkungen verfügt. Implementieren Sie einen exponentiellen Backoff mit Jitter, wenn Sie 429 Antworten erhalten.

Welche Datenbank sollte ich für meine Shopify-App in der Produktion verwenden?

Ersetzen Sie für die Produktion das Standard-SQLite durch PostgreSQL. Der Prisma-Sitzungsspeicheradapter funktioniert mit PostgreSQL – ändern Sie die Verbindungszeichenfolge in Ihren Umgebungsvariablen und aktualisieren Sie prisma/schema.prisma, um den Anbieter postgresql zu verwenden. Erwägen Sie bei Apps mit hohem Sitzungsvolumen Redis für die Sitzungsspeicherung anstelle von PostgreSQL, um die Datenbanklast auf dem Hot-Authentifizierungspfad zu reduzieren.

Wie stelle ich eine mit Remix erstellte Shopify-App bereit?

Shopify empfiehlt die Bereitstellung auf Plattformen, die die Node.js-Laufzeit unterstützen: Fly.io, Render, Railway, AWS (EC2/ECS) oder Google Cloud Run. Vercel und Netlify funktionieren für das Frontend, können jedoch nicht den persistenten Node.js-Server ausführen, der für den Sitzungsspeicher von Shopify erforderlich ist. Stellen Sie sicher, dass Ihre Bereitstellungsplattform lang laufende Prozesse für die Webhook-Verarbeitung und Hintergrundjobs unterstützt.


Nächste Schritte

Der Aufbau einer benutzerdefinierten Shopify-App, die Authentifizierung, GraphQL-Datenverwaltung, Webhooks und Abrechnung korrekt handhabt, erfordert umfassende Plattformkenntnisse. Eine falsch konfigurierte App besteht die Überprüfung im App Store nicht, beschädigt Händlershops oder legt Sicherheitslücken offen.

Die Shopify-App-Entwicklungsdienste von ECOSIRE decken den gesamten Entwicklungslebenszyklus ab: Architekturdesign, Remix/Polaris-Implementierung, GraphQL-API-Integration, Webhook-Infrastruktur, Abrechnungseinrichtung, App-Store-Einreichung und Post-Launch-Support.

Besprechen Sie Ihre individuellen Shopify-App-Anforderungen mit unserem Entwicklungsteam.

E

Geschrieben von

ECOSIRE Research and Development Team

Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.

Chatten Sie auf WhatsApp