effect-client-wrapper について
Effect でサードパーティー SDK のクライアントをラップする TypeScript のパターンです。クライアント呼び出しに型付きエラー処理、トレース、設定、依存性注入を導入したい開発者向けです。
主な機能
- 01型付きクライアントエラー
- 02use 呼び出しごとのトレース
- 03Redacted による API キー設定
- 04依存性注入
- 05クライアントアクセスのカプセル化
活用シーン
- 01SDK 呼び出しのエラー処理の統一
- 02クライアント操作のトレース
- 03設定経由での API キー読み込み
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ rhyssullivan/ effect-client-wrapper-skill --skill effect-client-wrapperインストール後、Claude Code で/effect-client-wrapperを入力して読み込まれたか確認します。
詳細
Effect クライアントラッパーパターン
一貫したエラー処理、トレース、依存性注入のために、「use」パターンを使ってサードパーティーの SDK クライアントを Effect でラップします。
パターンの構造
import { Context, Data, Effect, Layer, Config, Redacted } from "effect";
// 1. Define tagged errors
export class MyClientError extends Data.TaggedError("MyClientError")<{
cause: unknown;
}> {}
export class MyClientInstantiationError extends Data.TaggedError(
"MyClientInstantiationError"
)<{
cause: unknown;
}> {}
// 2. Define service interface with `use` method
export type IMyClient = Readonly<{
client: ThirdPartyClient;
use: <A>(
fn: (client: ThirdPartyClient) => Promise<A>
) => Effect.Effect<A, MyClientError, never>;
}>;
// 3. Create the service implementation
const make = Effect.gen(function* () {
const apiKey = yield* Config.redacted("MY_CLIENT_API_KEY");
const client = yield* Effect.try({
try: () => new ThirdPartyClient(Redacted.value(apiKey)),
catch: (cause) => new MyClientInstantiationError({ cause }),
});
const use = <A>(fn: (client: ThirdPartyClient) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new MyClientError({ cause }),
}).pipe(Effect.withSpan(`my_client.${fn.name ?? "use"}`));
return { client, use };
});
// 4. Export as Context.Tag with Default layer
export class MyClient extends Context.Tag("MyClient")<MyClient, IMyClient>() {
static Default = Layer.effect(this, make).pipe(
Layer.annotateSpans({ module: "MyClient" })
);
}
使用方法
const program = Effect.gen(function* () {
const myClient = yield* MyClient;
const result = yield* myClient.use((client) =>
client.someMethod({ param: "value" })
);
return result;
});
// Run with layer
program.pipe(Effect.provide(MyClient.Default));
主なメリット
- 一元化されたエラー処理 - すべてのクライアントエラーを型付きの
MyClientErrorでラップします - 自動トレース - すべての
use呼び出しで関数名を含むスパンを作成します - Config ベースのシークレット - API キーを
Config.redactedで読み込みます - クリーンな DI - 利用側は
yield* MyClientで注入します - カプセル化 - 生のクライアントを
useインターフェースの背後に隠します
バリエーション
複数のエラー型
export class MyClientNetworkError extends Data.TaggedError("MyClientNetworkError")<{
cause: unknown;
}> {}
export class MyClientValidationError extends Data.TaggedError("MyClientValidationError")<{
message: string;
}> {}
const use = <A>(fn: (client: ThirdPartyClient) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => {
if (cause instanceof NetworkError) {
return new MyClientNetworkError({ cause });
}
return new MyClientError({ cause });
},
}).pipe(Effect.withSpan(`my_client.${fn.name ?? "use"}`));
名前付き操作
汎用的な use の代わりに、特定のメソッドを公開します。
export type IEmailClient = Readonly<{
sendEmail: (params: SendEmailParams) => Effect.Effect<EmailResult, EmailError>;
getEmail: (id: string) => Effect.Effect<Email, EmailError>;
}>;
const make = Effect.gen(function* () {
const resend = yield* ResendClient;
return {
sendEmail: (params) =>
resend
.use((client) => client.emails.send(params))
.pipe(Effect.withSpan("email_client.send")),
getEmail: (id) =>
resend
.use((client) => client.emails.get(id))
.pipe(Effect.withSpan("email_client.get")),
};
});
リトライポリシーを使用する
import { Schedule } from "effect";
const retryPolicy = Schedule.exponential(100).pipe(
Schedule.intersect(Schedule.recurs(3)),
Schedule.jittered
);
const use = <A>(fn: (client: ThirdPartyClient) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new MyClientError({ cause }),
}).pipe(
Effect.retry(retryPolicy),
Effect.withSpan(`my_client.${fn.name ?? "use"}`)
);
実例:Stripe
import Stripe from "stripe";
import { Context, Data, Effect, Layer, Config, Redacted } from "effect";
export class StripeError extends Data.TaggedError("StripeError")<{
cause: unknown;
}> {}
export type IStripeClient = Readonly<{
use: <A>(fn: (stripe: Stripe) => Promise<A>) => Effect.Effect<A, StripeError>;
}>;
const make = Effect.gen(function* () {
const secretKey = yield* Config.redacted("STRIPE_SECRET_KEY");
const client = new Stripe(Redacted.value(secretKey));
const use = <A>(fn: (stripe: Stripe) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new StripeError({ cause }),
}).pipe(Effect.withSpan(`stripe.${fn.name ?? "use"}`));
return { use };
});
export class StripeClient extends Context.Tag("StripeClient")<
StripeClient,
IStripeClient
>() {
static Default = Layer.effect(this, make).pipe(
Layer.annotateSpans({ module: "StripeClient" })
);
}
// Usage
const createCustomer = Effect.gen(function* () {
const stripe = yield* StripeClient;
const customer = yield* stripe.use((client) =>
client.customers.create({ email: "[email protected]" })
);
return customer;
});
コメント
Markdown 対応