typed-service-contracts について
CLI やライブラリの開発向けに、契約と垂直スライスでアプリケーションロジックを構成する設計スキルです。Zod で入力を解析し、ハンドラーでは例外を投げずに Result に変換します。
主な機能
- 01Spec で入出力とエラーの契約を定義
- 02Zod による入力の解析と変換
- 03Handler で副作用のある処理を実装
- 04Result で成功と失敗を表現
- 05契約テストとロジックテストを分離
活用シーン
- 01CLI の入力境界を厳密に定義
- 02ファイルパスなどの入力を解析・検証
- 03エラー変換と業務ロジックの結果をテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ google-labs-code/ stitch-sdk --skill typed-service-contractsインストール後、Claude Code で/typed-service-contractsを入力して読み込まれたか確認します。
詳細
型付きサービスコントラクト(Spec & Handler パターン)
このスキルでは、Design by Contract(DbC)の原則に基づく垂直スライスアーキテクチャを定義します。アプリケーションロジックを厳密に定義された作業単位として扱い、入力は単に検証するのではなく解析し、エラーは例外ではなく値(Result パターン)として扱います。
このスキルを使用する場面
- CLI やライブラリの構築: ユーザー入力とシステムロジックの間に厳格な境界が必要な場合。
- 複雑な検証: 入力を使用可能な状態にする前に変換(解析)が必要な場合(例:文字列が有効なファイルパスであることの確認)。
- 高い信頼性が求められる要件: 未処理のランタイム例外を許容できず、網羅的なエラー処理が必要な場合。
- テスト重視: データ検証テストとビジネスロジックのテストを分離したい場合。
アーキテクチャの構成要素
1. Spec(spec.ts)
「コントラクト」または「ポート」です。_何をするか_を定義します。次の内容を含める必要があります。
- 入力スキーマ: 生の入力を有効な DTO に解析する Zod スキーマ。
- 出力スキーマ: 成功時のデータ構造を定義する Zod スキーマ。
- エラースキーマ: 特定の失敗モードを表す判別可能なユニオン(汎用エラーではありません)。
- Result 型:
Success | FailureのDiscriminatedUnion。 - インターフェース: 機能の定義(例:
interface ConfigureSpec)。
2. Handler(handler.ts)
「実装」または「アダプター」です。_どのように実現するか_を定義します。次の要件を満たす必要があります。
- Spec で定義されたインターフェースを実装する。
- 副作用(ファイルシステム、API 呼び出し)を処理する「不純な」クラスである。
- 例外を決してスローしない。内部エラーを捕捉し、
Result型にマッピングする必要があります。
使用方法
ステップ 1:コントラクトを定義する(spec.ts)
次のテンプレートに従って境界を定義します。
import { z } from "zod";
// 1. VALIDATION HELPERS (Reusable Refinements)
export const SafePathSchema = z
.string()
.min(1)
.refine((p) => !p.includes(".."), "No traversal allowed");
// 2. INPUT (The Command) - "Parse, don't validate"
export const MyTaskInputSchema = z.object({
path: SafePathSchema,
force: z.boolean().default(false),
});
export type MyTaskInput = z.infer<typeof MyTaskInputSchema>;
// 3. ERROR CODES (Exhaustive)
export const MyTaskErrorCode = z.enum([
"FILE_NOT_FOUND",
"PERMISSION_DENIED",
"UNKNOWN_ERROR",
]);
// 4. RESULT (The Monad)
export const MyTaskSuccess = z.object({
success: z.literal(true),
data: z.string(), // The output payload
});
export const MyTaskFailure = z.object({
success: z.literal(false),
error: z.object({
code: MyTaskErrorCode,
message: z.string(),
suggestion: z.string().optional(),
recoverable: z.boolean(),
}),
});
export type MyTaskResult =
| z.infer<typeof MyTaskSuccess>
| z.infer<typeof MyTaskFailure>;
// 5. INTERFACE (The Capability)
export interface MyTaskSpec {
execute(input: MyTaskInput): Promise<MyTaskResult>;
}
ステップ 2:Handler を実装する(handler.ts)
次のテンプレートに従ってロジックを実装します。
import { MyTaskSpec, MyTaskInput, MyTaskResult } from "./spec.js";
import * as fs from "fs";
export class MyTaskHandler implements MyTaskSpec {
async execute(input: MyTaskInput): Promise<MyTaskResult> {
try {
// 1. Business Logic
if (!fs.existsSync(input.path)) {
// 2. Explicit Error Return (No Throwing)
return {
success: false,
error: {
code: "FILE_NOT_FOUND",
message: `Path does not exist: ${input.path}`,
recoverable: true,
},
};
}
// 3. Success Return
return {
success: true,
data: "Operation complete",
};
} catch (error) {
// 4. Safety Net: Catch unknown runtime errors
return {
success: false,
error: {
code: "UNKNOWN_ERROR",
message: error instanceof Error ? error.message : String(error),
recoverable: false,
},
};
}
}
}
ステップ 3:テスト戦略
モノリシックなテストは書かないでください。コントラクトテストとロジックテストに分割します。
A. コントラクトテスト(スキーマ)
_門番_をテストします。無効なデータがハンドラーに到達する前に拒否されることを確認します。
- 重点項目: エッジケース、検証ルール、Zod のリファインメント。
- スタイル: データ駆動型(テーブルテスト)。
// spec.test.ts
import { MyTaskInputSchema } from "./spec";
const invalidCases = [
{ val: "../etc/passwd", err: "No traversal allowed" },
{ val: "", err: "min(1)" },
];
test.each(invalidCases)("validates paths", ({ val, err }) => {
const result = MyTaskInputSchema.safeParse({ path: val });
expect(result.success).toBe(false);
});
B. ロジックテスト(Handler)
_シェフ_をテストします。外部依存関係(fs、ネットワーク)をモックし、Result オブジェクトをアサートします。
- 重点項目: ビジネスロジックのフロー、エラーマッピング、成功状態。
- スタイル: モックを使用した単体テスト、またはシナリオランナー。
// handler.test.ts
import { MyTaskHandler } from "./handler";
import { vi } from "vitest"; // or jest
test("returns FILE_NOT_FOUND if path missing", async () => {
// MOCK
vi.mocked(fs.existsSync).mockReturnValue(false);
// EXECUTE
const handler = new MyTaskHandler();
const result = await handler.execute({ path: "/fake" });
// ASSERT (Check the Result Object)
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.code).toBe("FILE_NOT_FOUND");
}
});
コメント
Markdown 対応