Trigger.dev:バックグラウンドジョブフレームワーク完全ガイド
Trigger.dev:バックグラウンドジョブフレームワーク完全ガイド
バックグラウンドジョブの管理は、スケーラブルなアプリケーション開発において重要な要素です。長時間実行される処理、定期的なタスク、外部APIとの連携など、様々な場面でバックグラウンドジョブが必要になります。
Trigger.devは、TypeScriptファーストなバックグラウンドジョブフレームワークで、開発者体験と信頼性を重視した設計が特徴です。この記事では、Trigger.dev v3の基本から実践的な使い方まで、詳しく解説していきます。
Trigger.devとは
Trigger.devは、長時間実行されるバックグラウンドジョブとワークフローを構築するためのプラットフォームです。
主な特徴
- TypeScriptファースト: 完全な型安全性
- 長時間実行対応: 数時間〜数日の処理も可能
- 視覚的なダッシュボード: リアルタイム監視
- 自動リトライ: エラー時の再試行
- スケジューリング: Cron式対応
- 統合: Next.js、Remix、Express、Fastifyなど
- ローカル開発: 充実した開発ツール
v3の新機能(2025〜)
- より高速な実行: パフォーマンスが大幅向上
- 改善された課金モデル: より予測可能な料金体系
- 新しいSDK: よりシンプルなAPI
- エッジ対応: Cloudflare Workers対応
競合との比較
Trigger.dev vs Inngest
- Trigger.devは長時間ジョブに強い
- Inngestはイベント駆動に特化
- 料金体系が異なる(実行時間 vs ステップ数)
Trigger.dev vs BullMQ
- Trigger.devはクラウドホスト、BullMQはセルフホスト
- Trigger.devは開発者体験が優れている
- BullMQは無料でRedisベース
セットアップ
インストール
npm install @trigger.dev/sdk@latest
# または
pnpm add @trigger.dev/sdk@latest
CLIのインストール
npm install -g @trigger.dev/cli@latest
プロジェクトの初期化
# Next.jsプロジェクトの場合
npx trigger.dev@latest init
# 対話形式で設定
? What is your Trigger.dev API key? [your-api-key]
? Where should we create the trigger directory? trigger/
? Which framework are you using? Next.js (App Router)
環境変数
.env.local に以下を追加します。
# Trigger.dev
TRIGGER_API_KEY=your_api_key
TRIGGER_API_URL=https://api.trigger.dev
Trigger.devのダッシュボード(https://cloud.trigger.dev/)でAPIキーを取得します。
基本的な使い方
最初のタスク
シンプルなバックグラウンドタスクを作成してみましょう。
// trigger/example.ts
import { task } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload: { name: string }) => {
console.log(`Hello, ${payload.name}!`);
return {
message: `Hello, ${payload.name}!`,
timestamp: new Date().toISOString(),
};
},
});
タスクのトリガー
Next.jsのAPI routeからタスクを実行します。
// app/api/trigger-hello/route.ts
import { helloWorldTask } from "@/trigger/example";
export async function POST(request: Request) {
const { name } = await request.json();
const handle = await helloWorldTask.trigger({
name,
});
return Response.json({
id: handle.id,
message: "Task triggered successfully",
});
}
Next.jsとの統合
Next.jsのApp Routerと統合するには、専用のエンドポイントを作成します。
// app/api/trigger/route.ts
import { createAppRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger/client";
// タスクをインポート
import "@/trigger/example";
export const { POST, dynamic } = createAppRoute(client);
クライアントの設定
// trigger/client.ts
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "my-app",
apiKey: process.env.TRIGGER_API_KEY!,
apiUrl: process.env.TRIGGER_API_URL,
});
タスクの詳細
ペイロードとバリデーション
Zodを使ってペイロードを検証できます。
// trigger/tasks/send-email.ts
import { task } from "@trigger.dev/sdk/v3";
import { z } from "zod";
const payloadSchema = z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
});
export const sendEmailTask = task({
id: "send-email",
run: async (payload: z.infer<typeof payloadSchema>) => {
// ペイロード検証
const validated = payloadSchema.parse(payload);
// メール送信処理
const result = await sendEmail({
to: validated.to,
subject: validated.subject,
body: validated.body,
});
return { success: true, messageId: result.id };
},
});
リトライ設定
// trigger/tasks/api-call.ts
import { task, retry } from "@trigger.dev/sdk/v3";
export const apiCallTask = task({
id: "api-call",
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
randomize: true,
},
run: async (payload: { url: string }) => {
const response = await fetch(payload.url);
if (!response.ok) {
// リトライをトリガー
throw new Error(`API call failed: ${response.status}`);
}
return await response.json();
},
});
タイムアウト設定
export const longRunningTask = task({
id: "long-running",
timeout: "30m", // 30分でタイムアウト
run: async (payload) => {
// 長時間実行される処理
await processLargeDataset(payload.datasetId);
return { success: true };
},
});
スケジュールタスク
Cron式でのスケジュール
// trigger/tasks/daily-report.ts
import { task, schedules } from "@trigger.dev/sdk/v3";
export const dailyReportTask = task({
id: "daily-report",
run: async () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const stats = await db.getStatsForDate(yesterday);
await sendEmail({
to: "admin@example.com",
subject: "Daily Report",
body: formatReport(stats),
});
return { success: true, stats };
},
});
// スケジュール設定
schedules.create({
task: dailyReportTask,
cron: "0 9 * * *", // 毎日9時
timezone: "Asia/Tokyo",
});
インターバルスケジュール
import { task, schedules } from "@trigger.dev/sdk/v3";
export const healthCheckTask = task({
id: "health-check",
run: async () => {
const services = await checkAllServices();
const failedServices = services.filter((s) => !s.healthy);
if (failedServices.length > 0) {
await sendAlert({
message: `Services down: ${failedServices.map((s) => s.name).join(", ")}`,
});
}
return { healthy: failedServices.length === 0 };
},
});
// 5分ごとに実行
schedules.create({
task: healthCheckTask,
interval: "5m",
});
並列処理とバッチ
並列タスク実行
// trigger/tasks/batch-processing.ts
import { task, batch } from "@trigger.dev/sdk/v3";
export const processImageTask = task({
id: "process-image",
run: async (payload: { imageUrl: string }) => {
const processed = await processImage(payload.imageUrl);
return { success: true, url: processed.url };
},
});
export const batchImageProcessing = task({
id: "batch-image-processing",
run: async (payload: { imageUrls: string[] }) => {
// 並列実行(最大10個まで同時)
const results = await batch.run(
payload.imageUrls.map((url) => ({
task: processImageTask,
payload: { imageUrl: url },
})),
{ maxConcurrency: 10 }
);
const successful = results.filter((r) => r.ok);
const failed = results.filter((r) => !r.ok);
return {
total: results.length,
successful: successful.length,
failed: failed.length,
};
},
});
チャンク処理
大量のデータを分割して処理します。
// trigger/tasks/bulk-email.ts
import { task } from "@trigger.dev/sdk/v3";
export const sendBulkEmailTask = task({
id: "send-bulk-email",
run: async (payload: { userIds: string[]; template: string }) => {
const CHUNK_SIZE = 100;
const chunks = [];
// ユーザーIDをチャンクに分割
for (let i = 0; i < payload.userIds.length; i += CHUNK_SIZE) {
chunks.push(payload.userIds.slice(i, i + CHUNK_SIZE));
}
let sent = 0;
let failed = 0;
// 各チャンクを処理
for (const chunk of chunks) {
const users = await db.user.findMany({
where: { id: { in: chunk } },
});
for (const user of users) {
try {
await sendEmail({
to: user.email,
template: payload.template,
data: { name: user.name },
});
sent++;
} catch (error) {
console.error(`Failed to send to ${user.email}:`, error);
failed++;
}
}
// レート制限対策で少し待つ
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return { sent, failed, total: payload.userIds.length };
},
});
Webhookとの統合
Webhook受信タスク
// trigger/tasks/webhook-handler.ts
import { task } from "@trigger.dev/sdk/v3";
export const stripeWebhookTask = task({
id: "stripe-webhook",
run: async (payload: { event: any }) => {
const { type, data } = payload.event;
switch (type) {
case "payment_intent.succeeded":
await handlePaymentSuccess(data.object);
break;
case "payment_intent.failed":
await handlePaymentFailure(data.object);
break;
case "customer.subscription.created":
await handleSubscriptionCreated(data.object);
break;
default:
console.log(`Unhandled event type: ${type}`);
}
return { processed: true, type };
},
});
async function handlePaymentSuccess(paymentIntent: any) {
await db.payment.update({
where: { stripeId: paymentIntent.id },
data: { status: "succeeded" },
});
await sendReceiptEmail(paymentIntent.customer);
}
async function handlePaymentFailure(paymentIntent: any) {
await db.payment.update({
where: { stripeId: paymentIntent.id },
data: { status: "failed" },
});
await sendPaymentFailedEmail(paymentIntent.customer);
}
async function handleSubscriptionCreated(subscription: any) {
await db.subscription.create({
data: {
stripeId: subscription.id,
customerId: subscription.customer,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
Webhook APIエンドポイント
// app/api/webhooks/stripe/route.ts
import { stripeWebhookTask } from "@/trigger/tasks/webhook-handler";
import { stripe } from "@/lib/stripe";
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response("Webhook signature verification failed", {
status: 400,
});
}
// バックグラウンドで処理
await stripeWebhookTask.trigger({ event });
return new Response("Webhook received", { status: 200 });
}
実践例
メール送信キュー
// trigger/tasks/email-queue.ts
import { task } from "@trigger.dev/sdk/v3";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
interface EmailPayload {
to: string;
subject: string;
html: string;
from?: string;
}
export const sendEmailTask = task({
id: "send-email",
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1000,
},
run: async (payload: EmailPayload) => {
const result = await resend.emails.send({
from: payload.from || "noreply@example.com",
to: payload.to,
subject: payload.subject,
html: payload.html,
});
if (result.error) {
throw new Error(`Failed to send email: ${result.error.message}`);
}
// ログを記録
await db.emailLog.create({
data: {
to: payload.to,
subject: payload.subject,
status: "sent",
messageId: result.data!.id,
sentAt: new Date(),
},
});
return { success: true, messageId: result.data!.id };
},
});
// 使用例
export const welcomeEmailTask = task({
id: "welcome-email",
run: async (payload: { userId: string }) => {
const user = await db.user.findUnique({
where: { id: payload.userId },
});
if (!user) {
throw new Error("User not found");
}
await sendEmailTask.trigger({
to: user.email,
subject: "Welcome to Our Platform!",
html: `
<h1>Welcome, ${user.name}!</h1>
<p>Thank you for signing up.</p>
`,
});
return { success: true };
},
});
データ同期タスク
// trigger/tasks/data-sync.ts
import { task, schedules } from "@trigger.dev/sdk/v3";
export const syncUsersTask = task({
id: "sync-users",
timeout: "1h",
run: async () => {
let page = 1;
let hasMore = true;
let totalSynced = 0;
while (hasMore) {
// 外部APIからユーザーデータ取得
const response = await fetch(
`https://api.external.com/users?page=${page}&limit=100`,
{
headers: {
Authorization: `Bearer ${process.env.EXTERNAL_API_TOKEN}`,
},
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
const users = data.users;
// データベースに保存
for (const user of users) {
await db.user.upsert({
where: { externalId: user.id },
update: {
name: user.name,
email: user.email,
updatedAt: new Date(),
},
create: {
externalId: user.id,
name: user.name,
email: user.email,
},
});
totalSynced++;
}
hasMore = data.hasMore;
page++;
// レート制限対策
await new Promise((resolve) => setTimeout(resolve, 500));
}
return {
success: true,
totalSynced,
pages: page - 1,
};
},
});
// 毎時実行
schedules.create({
task: syncUsersTask,
cron: "0 * * * *",
});
画像処理パイプライン
// trigger/tasks/image-pipeline.ts
import { task } from "@trigger.dev/sdk/v3";
import sharp from "sharp";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
export const processImageTask = task({
id: "process-image",
run: async (payload: { imageUrl: string; imageId: string }) => {
// 画像ダウンロード
const response = await fetch(payload.imageUrl);
const imageBuffer = Buffer.from(await response.arrayBuffer());
// 各サイズの画像を作成
const sizes = [
{ name: "thumbnail", width: 150, height: 150 },
{ name: "medium", width: 800, height: 800 },
{ name: "large", width: 1920, height: 1920 },
];
const results = await Promise.all(
sizes.map(async (size) => {
const processed = await sharp(imageBuffer)
.resize(size.width, size.height, {
fit: "inside",
withoutEnlargement: true,
})
.jpeg({ quality: 85 })
.toBuffer();
const key = `images/${payload.imageId}/${size.name}.jpg`;
await s3.send(
new PutObjectCommand({
Bucket: "my-bucket",
Key: key,
Body: processed,
ContentType: "image/jpeg",
})
);
return {
size: size.name,
key,
url: `https://cdn.example.com/${key}`,
};
})
);
// データベース更新
await db.image.update({
where: { id: payload.imageId },
data: {
thumbnailUrl: results.find((r) => r.size === "thumbnail")?.url,
mediumUrl: results.find((r) => r.size === "medium")?.url,
largeUrl: results.find((r) => r.size === "large")?.url,
processed: true,
},
});
return { success: true, results };
},
});
レポート生成
// trigger/tasks/report-generation.ts
import { task, schedules } from "@trigger.dev/sdk/v3";
import { PDFDocument } from "pdf-lib";
export const generateMonthlyReportTask = task({
id: "generate-monthly-report",
timeout: "30m",
run: async (payload: { month: string; year: number }) => {
// データ収集
const startDate = new Date(payload.year, parseInt(payload.month) - 1, 1);
const endDate = new Date(payload.year, parseInt(payload.month), 0);
const [users, orders, revenue] = await Promise.all([
db.user.count({
where: {
createdAt: { gte: startDate, lte: endDate },
},
}),
db.order.count({
where: {
createdAt: { gte: startDate, lte: endDate },
},
}),
db.order.aggregate({
_sum: { amount: true },
where: {
createdAt: { gte: startDate, lte: endDate },
status: "completed",
},
}),
]);
// PDFレポート作成
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
page.drawText(`Monthly Report - ${payload.month}/${payload.year}`, {
x: 50,
y: 750,
size: 20,
});
page.drawText(`New Users: ${users}`, { x: 50, y: 700, size: 14 });
page.drawText(`Total Orders: ${orders}`, { x: 50, y: 680, size: 14 });
page.drawText(`Revenue: $${revenue._sum.amount || 0}`, {
x: 50,
y: 660,
size: 14,
});
const pdfBytes = await pdfDoc.save();
// S3にアップロード
const key = `reports/${payload.year}/${payload.month}/monthly-report.pdf`;
await s3.send(
new PutObjectCommand({
Bucket: "my-reports-bucket",
Key: key,
Body: pdfBytes,
ContentType: "application/pdf",
})
);
// 管理者に通知
await sendEmailTask.trigger({
to: "admin@example.com",
subject: `Monthly Report Ready - ${payload.month}/${payload.year}`,
html: `
<p>The monthly report is ready.</p>
<a href="https://reports.example.com/${key}">Download Report</a>
`,
});
return {
success: true,
reportUrl: `https://reports.example.com/${key}`,
stats: { users, orders, revenue: revenue._sum.amount },
};
},
});
// 毎月1日の午前9時に実行
schedules.create({
task: generateMonthlyReportTask,
cron: "0 9 1 * *",
timezone: "Asia/Tokyo",
});
ローカル開発
開発サーバーの起動
npm run dev:trigger
# または
npx trigger.dev@latest dev
このコマンドで、ローカルの開発環境とTrigger.devが接続されます。
ダッシュボード
Trigger.devのダッシュボード(https://cloud.trigger.dev/)で、以下が確認できます。
- タスクの実行履歴
- リアルタイムログ
- エラーとリトライ
- スケジュール一覧
テスト実行
// scripts/test-task.ts
import { helloWorldTask } from "@/trigger/example";
async function test() {
const handle = await helloWorldTask.trigger({
name: "Test User",
});
console.log("Task triggered:", handle.id);
// 結果を待つ
const result = await handle.wait();
console.log("Result:", result);
}
test().catch(console.error);
実行:
npx tsx scripts/test-task.ts
デプロイ
Vercelへのデプロイ
# 環境変数を設定
vercel env add TRIGGER_API_KEY
# デプロイ
vercel --prod
他のプラットフォーム
- Netlify: Netlify Functions
- Railway: Node.jsアプリとしてデプロイ
- Render: Webサービスとしてデプロイ
ベストプラクティス
1. タスクを小さく保つ
// Good: 各タスクは1つの責任
export const processOrder = task({
id: "process-order",
run: async (payload) => {
await validateOrder(payload.orderId);
await chargePayment(payload.orderId);
await sendConfirmation(payload.orderId);
},
});
// Better: タスクを分割
export const validateOrderTask = task({ /* ... */ });
export const chargePaymentTask = task({ /* ... */ });
export const sendConfirmationTask = task({ /* ... */ });
2. 冪等性を確保
export const processPaymentTask = task({
id: "process-payment",
run: async (payload: { paymentId: string }) => {
// すでに処理済みかチェック
const existing = await db.payment.findUnique({
where: { id: payload.paymentId },
});
if (existing?.status === "completed") {
return { message: "Already processed" };
}
// 処理実行
const result = await processPayment(payload.paymentId);
return result;
},
});
3. エラーハンドリング
export const robustTask = task({
id: "robust-task",
run: async (payload) => {
try {
const result = await riskyOperation(payload);
return { success: true, result };
} catch (error) {
// ログ記録
await db.errorLog.create({
data: {
taskId: "robust-task",
error: error.message,
payload,
},
});
// 特定のエラーはリトライしない
if (error instanceof ValidationError) {
return { success: false, error: error.message };
}
// その他はリトライ
throw error;
}
},
});
まとめ
Trigger.devは、TypeScriptファーストなバックグラウンドジョブフレームワークです。主な利点は以下の通りです。
- 開発者体験: 型安全で直感的なAPI
- 信頼性: 自動リトライと監視
- 長時間実行: 数時間〜数日の処理も対応
- 可視性: リアルタイムダッシュボード
複雑なバックグラウンド処理を簡単に構築でき、ローカル開発環境も充実しています。スケーラブルなアプリケーション開発において、Trigger.devは強力な選択肢となるでしょう。