Shopify Functions
Shopify Functions は Shopify プラットフォームで最も強力なエクステンションポイントです。Shopify のインフラストラクチャ内でカスタムロジックを実行し、コアのコマースオペレーション(ディスカウント、配送料、決済方法、カートトランスフォーメーション、注文ルーティングなど)をカスタマイズできます。Functions は WebAssembly モジュールとして5ミリ秒以内に実行されるため、パフォーマンスに影響を与えることなくすべてのチェックアウトで実行できます。このレッスンでは、アーキテクチャ、エクステンションターゲット、開発ワークフロー、非推奨の Shopify Scripts からの重要な移行をカバーします。
Shopify Functions とは?
Functions は、Shopify のコマースロジックをインターセプトして変更するサーバーレスのマイクロプログラムと考えてください。自前のサーバーで実行し API を介して通信するアプリとは異なり、Functions はサンドボックス化された WebAssembly(Wasm)モジュールとして Shopify のサーバー上で実行されます。
Functions が重要な理由
- ネットワークレイテンシーなし: コードはデータのすぐそばの Shopify サーバーで実行
- パフォーマンス保証: 厳格な実行制限(5ms、11MB メモリ)がチェックアウト速度を保証
- 信頼性: 外部サーバーのダウンも、Webhook 配信の失敗もなし
- スケール: トラフィック量に関係なくすべてのチェックアウトで実行
- セキュリティ: サンドボックス化された Wasm はネットワーク、ファイルシステム、他のストアにアクセス不可
Shopify Scripts(Ruby ベースのチェックアウトカスタマイズ、以前は Plus 限定)は非推奨であり、2026年6月に削除されます。Scripts を使用しているすべてのマーチャントは Functions に移行する必要があります。クライアントのために Scripts を維持している場合、移行計画を今すぐ始めるべきです。
エクステンションターゲット
各 Function はコマースフローの特定の部分をターゲットにします。利用可能なターゲットは以下の通りです:
| エクステンションターゲット | カスタマイズする内容 | ユースケースの例 |
|---|---|---|
purchase.product-discount.run | 商品レベルのディスカウント | 「2個買うと10%オフ」 |
purchase.order-discount.run | 注文レベルのディスカウント | 「100ドル以上の注文で20%オフ」 |
purchase.shipping-discount.run | 配送ディスカウント | 「50ドル以上で送料無料」 |
purchase.payment-customization.run | 決済方法の表示/非表示 | 海外注文の代引きを非表示 |
purchase.delivery-customization.run | 配送オプションのカスタマイズ | 配送料のリネーム、並べ替え、非表示 |
cart-transform.run | カートの変更 | 商品の自動バンドル、ラインアイテムの統合 |
purchase.validation.run | カート/チェックアウトのバリデーション | 数量制限、住所ルールの適用 |
fulfillment-constraints.run | フルフィルメントルーティング | 特定ロケーションへの注文ルーティング |
order-routing.run | 注文ルーティングロジック | 最寄りの倉庫への注文送信 |
開発ワークフロー
Function の作成
Shopify CLI を使用して新しい Function をスキャフォールドします:
# アプリディレクトリに移動
cd ~/shopify-projects/masterclass-app
# Function エクステンションを生成
shopify app generate extension --template product_discounts --name "volume-discount"
これにより extensions/ フォルダに新しいディレクトリが作成されます:
extensions/volume-discount/
├── src/
│ └── run.js # Function ロジック(Rust の場合は run.rs)
├── input.graphql # 入力クエリ定義
├── shopify.extension.toml # エクステンション設定
└── package.json # 依存関係(JS のみ)
入力クエリ
すべての Function は、Function が受け取るデータを定義する GraphQL 入力クエリから始まります。これは Function の実行前に実行され、利用可能な情報を決定します。
# extensions/volume-discount/input.graphql
query Input {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
id
product {
id
title
hasAnyTag(tags: ["volume-eligible"])
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
}
}
}
discountNode {
metafield(namespace: "volume-discount", key: "config") {
value
}
}
}
入力クエリはすべてのチェックアウトで実行されます。最小限に保ちましょう -- Function が実際に必要とするデータのみをリクエストしてください。不要なフィールドはレイテンシーとメモリ使用量を増加させます。
JavaScript で Functions を書く
JavaScript Functions は @shopify/shopify_function パッケージを使用します:
// extensions/volume-discount/src/run.js
import { DiscountApplicationStrategy } from "../generated/api";
/**
* ボリュームディスカウント:対象商品を3個以上購入すると割引率が適用されます。
*/
export function run(input) {
// メタフィールドから設定を解析
const config = JSON.parse(
input?.discountNode?.metafield?.value ?? '{"quantity": 3, "percentage": 10}'
);
const targets = [];
for (const line of input.cart.lines) {
const variant = line.merchandise;
// 商品以外のライン(ギフトカードなど)をスキップ
if (variant.__typename !== "ProductVariant") continue;
// 商品がボリュームディスカウントの対象かチェック
if (!variant.product.hasAnyTag) continue;
// 数量がしきい値を満たすかチェック
if (line.quantity >= config.quantity) {
targets.push({
productVariant: {
id: variant.id,
},
});
}
}
// 対象がなければ空のディスカウントを返す
if (targets.length === 0) {
return { discounts: [], discountApplicationStrategy: DiscountApplicationStrategy.First };
}
return {
discounts: [
{
value: {
percentage: {
value: String(config.percentage),
},
},
targets,
message: `${config.percentage}% off when you buy ${config.quantity}+`,
},
],
discountApplicationStrategy: DiscountApplicationStrategy.First,
};
}
Rust で Functions を書く
Rust Functions はより小さく高速な Wasm モジュールにコンパイルされます。最大のパフォーマンスが必要な場合や Rust に慣れている場合に最適です:
// extensions/volume-discount/src/run.rs
use shopify_function::prelude::*;
use shopify_function::Result;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Config {
quantity: i64,
percentage: f64,
}
impl Default for Config {
fn default() -> Self {
Config {
quantity: 3,
percentage: 10.0,
}
}
}
generate_types!(
query_path = "./input.graphql",
schema_path = "./schema.graphql"
);
#[shopify_function_target(query_path = "./input.graphql", schema_path = "./schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let config: Config = input
.discount_node
.metafield
.as_ref()
.and_then(|m| serde_json::from_str(&m.value).ok())
.unwrap_or_default();
let mut targets = vec![];
for line in &input.cart.lines {
if let input::InputCartLinesMerchandise::ProductVariant(variant) = &line.merchandise {
if variant.product.has_any_tag && line.quantity >= config.quantity {
targets.push(output::Target {
product_variant: Some(output::ProductVariantTarget {
id: variant.id.clone(),
}),
});
}
}
}
if targets.is_empty() {
return Ok(output::FunctionRunResult {
discounts: vec![],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
});
}
Ok(output::FunctionRunResult {
discounts: vec![output::Discount {
value: output::Value {
percentage: Some(output::Percentage {
value: config.percentage.to_string(),
}),
fixed_amount: None,
},
targets,
message: Some(format!(
"{}% off when you buy {}+",
config.percentage, config.quantity
)),
conditions: None,
}],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
})
}
JavaScript を選ぶ場合: チームが JS を知っている、ロジックがシンプル、素早いイテレーションが必要。JavaScript Functions は書きやすくデバッグしやすいです。
Rust を選ぶ場合: 最小の Wasm バイナリ、最大の実行速度が必要、または Rust に慣れている場合。Rust Functions は通常 50-100KB にコンパイル(JavaScript は 200-500KB)。
両方とも WebAssembly にコンパイルされ、同じサンドボックスで実行されます。典型的なディスカウント/配送ロジックではパフォーマンスの差は通常無視できます。
エクステンション設定
shopify.extension.toml ファイルが Function を設定します:
name = "volume-discount"
type = "product_discounts"
api_version = "2026-04"
[build]
command = "npm exec -- shopify app function build"
path = "dist/function.wasm"
watch = ["src/**/*.js"]
[ui]
enable_create = true
[ui.paths]
create = "/app/volume-discount/:functionId/create"
details = "/app/volume-discount/:functionId/:id"
[ui] セクションは重要です -- マーチャントがアプリの UI でディスカウントを設定する場所を Shopify に伝えます。マーチャントが Function を使って新しいディスカウントを作成すると、Shopify は ui.paths.create で指定された URL を埋め込みアプリ内でロードします。
Functions のテスト
ローカルテスト
デプロイ前に Functions をローカルでテストします:
# サンプル入力で Function を実行
shopify app function run --path extensions/volume-discount
# 特定の入力ファイルでテスト
echo '{"cart":{"lines":[{"quantity":5,"merchandise":{"__typename":"ProductVariant","id":"gid://shopify/ProductVariant/1","product":{"id":"gid://shopify/Product/1","title":"Widget","hasAnyTag":true}},"cost":{"amountPerQuantity":{"amount":"10.00","currencyCode":"USD"}}}]},"discountNode":{"metafield":{"value":"{\"quantity\":3,\"percentage\":15}"}}}' | shopify app function run --path extensions/volume-discount
ユニットテスト(JavaScript)
// extensions/volume-discount/src/run.test.js
import { describe, it, expect } from "vitest";
import { run } from "./run";
describe("volume-discount function", () => {
it("数量がしきい値を満たすとディスカウントを適用する", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};
const result = run(input);
expect(result.discounts).toHaveLength(1);
expect(result.discounts[0].value.percentage.value).toBe("10");
expect(result.discounts[0].targets).toHaveLength(1);
});
it("数量がしきい値以下の場合はディスカウントを返さない", () => {
const input = {
cart: {
lines: [
{
quantity: 1,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};
const result = run(input);
expect(result.discounts).toHaveLength(0);
});
it("対象外の商品をスキップする", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: false },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: { metafield: null },
};
const result = run(input);
expect(result.discounts).toHaveLength(0);
});
});
Scripts から Functions への移行
Shopify Scripts(Ruby ベース、Plus 限定)は非推奨で、2026年6月に削除期限が設定されています。あなたやクライアントが Scripts を使用している場合、移行は必須です。
移行タイムライン
| 日付 | マイルストーン |
|---|---|
| 2024年 | Functions GA、Scripts 非推奨を発表 |
| 2025年 | 新しい Scripts の作成不可 |
| 2026年1月 | 管理画面で移行ツール利用可能 |
| 2026年6月 | Scripts の完全削除 |
変更点
| Shopify Scripts | Shopify Functions |
|---|---|
| Ruby 言語 | JavaScript または Rust(Wasm にコンパイル) |
| Plus 限定 | すべてのプランで利用可能 |
| Script Editor アプリ | アプリに組み込み、またはスタンドアロン |
| ラインアイテム、配送、決済に限定 | より多くのエクステンションターゲット |
| 入力クエリなし | 明示的な GraphQL 入力クエリ |
| 可変カートオブジェクト | 不変入力、宣言的出力 |
移行戦略
- 既存の Scripts を監査: 使用中のすべての Scripts、そのタイプ(ラインアイテム、配送、決済)、ロジックをリストアップ。
- Function ターゲットにマッピング: ラインアイテムスクリプトは
product-discountまたはorder-discountに。配送スクリプトはdelivery-customizationに。決済スクリプトはpayment-customizationにマッピング。 - ロジックを書き直す: Ruby ロジックを JavaScript または Rust に変換。パラダイムが異なります -- Functions はカートオブジェクトを変更する代わりに不変入力と宣言的出力を使用。
- 設定 UI を構築: Scripts は Script Editor を使用;Functions にはアプリ内のマーチャント向け UI が必要。
- 徹底的にテスト: Function テストツールを使用し、Scripts が処理していたのと同じシナリオに対して検証。
- デプロイして有効化: ストアに Function をインストールし、Script を無効化。
移行には時間がかかります。特に多くの条件ルールを持つ複雑な Scripts の場合はなおさらです。今すぐ計画を始めましょう。開発ストアで Functions をテストし、2026年初頭に本番ストアへの展開を開始してください。最後の月まで待つと、マーチャントのチェックアウトに混乱が生じるリスクがあります。
移行の例:ラインアイテム Script から商品ディスカウント Function へ
元の Ruby Script(Script Editor):
Input.cart.line_items.each do |line_item|
if line_item.quantity >= 3
line_item.change_line_price(
line_item.line_price * 0.9,
message: "10% volume discount"
)
end
end
Output.cart = Input.cart
移行後の JavaScript Function:
export function run(input) {
const discounts = [];
for (const line of input.cart.lines) {
if (line.quantity >= 3) {
discounts.push({
value: { percentage: { value: "10" } },
targets: [{ productVariant: { id: line.merchandise.id } }],
message: "10% volume discount",
});
}
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}
Function はカートを直接変更する代わりに、不変の入力/出力パターンで同じ結果を達成します。
実行制限
Functions はチェックアウトのパフォーマンスを保護するために制約された環境で実行されます:
| 制限 | 値 |
|---|---|
| 実行時間 | 5ms |
| メモリ | 11 MB |
| Wasm バイナリサイズ | 256 KB |
| 入力サイズ | 64 KB |
| 出力サイズ | 64 KB |
| ネットワークアクセス | なし |
| ファイルシステムアクセス | なし |
Function ロジックをシンプルで焦点を絞ったものに保ちましょう。深いネスト、大きなデータ構造、複雑な文字列操作を避けてください。5ms の制限に達した場合、コードをプロファイリングして不要な反復を探しましょう。Rust Functions は一般的に JavaScript よりも高速でメモリ使用量が少ないです。
重要なポイント
- Shopify Functions は WebAssembly モジュールとして Shopify のインフラストラクチャ内でカスタムロジックを実行
- 高速(5ms以下)、信頼性が高く(外部サーバーなし)、スケーラブル(すべてのチェックアウトで実行)
- エクステンションターゲットはディスカウント、配送、決済、カートトランスフォーム、バリデーション、フルフィルメントをカバー
- JavaScript(より簡単)または Rust(より小さく高速)で Functions を記述
- 入力クエリが Function が受け取るデータを定義;出力は宣言的
- Scripts は非推奨 -- 2026年6月までに Functions に移行
shopify app function runとユニットテストでローカルテスト
これで Shopify 基礎モジュールは終了です。プラットフォームのアーキテクチャ、API、テンプレート、ヘッドレスオプション、エクステンションランタイムについて深い理解が得られました。次のモジュールでは、Claude Code が Shopify 開発ワークフローをどのように変革するかを探ります。