第18章:HTTP/外部APIを境界にする 🌐🚧
外部APIって、落ちる・遅い・仕様が変わるの三拍子がそろいがち…😵💫💥 だからこそこの章は、合言葉これです👇
「外部APIは “境界” の外!中(ルール)に混ぜない!」 📦➡️🌍✨
1) この章でできるようになること 🎯💖
- 外部API呼び出しを
IExternalApiClientみたいな境界で包めるようになる 🧩 - “中のロジック” を Fakeで爆速テストできるようになる 🧪⚡
- C#のHTTPまわりで **やりがちな事故(new HttpClient地獄など)**を避けられるようになる 🧯😇
- IHttpClientFactory + 回復性(resilience) の「今どきの基本形」を触れる 🤝✨ (.NET 10 は LTS で、2026-01-13時点の最新パッチは 10.0.2 になってます) (Microsoft)
2) 外部APIが “テストの敵” な理由 😈🌪️
外の世界はコントロール不能…!たとえば👇
- ネットワークが不安定でタイムアウトする 🐢💤
- 429(制限) や 500(障害) が返る 🤯
- 仕様変更でレスポンス形式が変わる 🔁💥
- たまに落ちる(そしてたまに復活する)👻
なので、重要ロジックの中に HttpClient 直書きすると… テストが「運ゲー」になりがちです 🎲😵💫
3) まず “やらない形” 🙅♀️💣(混ぜるとつらい)
- 重要ロジックの中で
new HttpClient()して叩く - 重要ロジックが
HttpResponseMessageやステータスコードの都合に引きずられる - あちこちにURLや認証ヘッダが散らばる
さらに、HttpClient をリクエストごとに作るのは ソケット枯渇につながる可能性があるので注意です⚠️(重い負荷で SocketException が出たり) (Microsoft Learn)
4) “正解の形” ✅🧩(境界で包む)

ポイントはこれ👇
-
中(ルール):
- 「何をしたいか」だけを書く(例:為替レートが欲しい、送料が欲しい、住所検証したい、など)
- HTTPの都合を知らない(URL/ヘッダ/JSON/ステータスコードを知らない)📦✨
-
外(I/O):
- HTTPで叩く、JSONを読む、認証する…を担当 🌐🔑
- 失敗するのもここ(遅い・落ちる)😵
その橋渡しが 境界インターフェースです👇
IExchangeRateApi/IShippingFeeApi/ICustomerScoreClientみたいなやつ ✨
5) ハンズオン:為替レートAPIを “境界化” してみる 💱🌐✨
ここでは例として「USD→JPYのレートを外部APIから取って、金額変換する」やつを作ります😊
5-1) 境界(インターフェース)を作る 🧩📮
public interface IExchangeRateApi
{
Task<decimal> GetUsdToJpyAsync(CancellationToken ct = default);
}
✅ ここ大事:戻り値は “中でほしい形” に寄せる
HttpResponseMessageとか返さない 🙅♀️- 「レート」だけ返す(中が使いやすい)🎯
5-2) 中(ルール側)のサービスを書く 📦✨
public sealed class CurrencyConverter
{
private readonly IExchangeRateApi _exchangeRateApi;
public CurrencyConverter(IExchangeRateApi exchangeRateApi)
{
_exchangeRateApi = exchangeRateApi;
}
public async Task<decimal> ConvertUsdToJpyAsync(decimal usd, CancellationToken ct = default)
{
if (usd < 0) throw new ArgumentOutOfRangeException(nameof(usd));
var rate = await _exchangeRateApi.GetUsdToJpyAsync(ct);
// ここは「ビジネスのルール」:レートが変でも計算しない、など
if (rate <= 0) throw new InvalidOperationException("Invalid exchange rate.");
return Math.Round(usd * rate, 0, MidpointRounding.AwayFromZero);
}
}
🌟この CurrencyConverter は HTTPを一切知らない ので、テストが超ラクになります🧪💖
5-3) 外(HTTP実装)を書く 🌐🔧
HttpClient は IHttpClientFactory(AddHttpClient) 経由で使うのが定番です。 (Microsoft Learn)
まず、外部APIのレスポンス(例)用DTOを作って:
public sealed record ExchangeRateResponse(Dictionary<string, decimal> rates);
次にクライアント実装:
using System.Net.Http.Json;
public sealed class ExchangeRateApiClient : IExchangeRateApi
{
private readonly HttpClient _http;
public ExchangeRateApiClient(HttpClient http)
{
_http = http;
}
public async Task<decimal> GetUsdToJpyAsync(CancellationToken ct = default)
{
// 例:GET /latest?base=USD&symbols=JPY みたいなAPIを想定
var res = await _http.GetFromJsonAsync<ExchangeRateResponse>(
"latest?base=USD&symbols=JPY",
ct);
if (res is null || res.rates is null) throw new HttpRequestException("Empty response.");
if (!res.rates.TryGetValue("JPY", out var jpy))
throw new HttpRequestException("JPY rate not found.");
return jpy;
}
}
✅ “HTTPの面倒” はぜんぶここで引き受ける感じです🌐🧹
6) 組み立て(DI登録)で本物を接続する 🔌🏗️✨
6-1) Typed Client として登録する(おすすめ)🧩
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHttpClient<IExchangeRateApi, ExchangeRateApiClient>(http =>
{
http.BaseAddress = new Uri("https://api.example.com/"); // 例
})
// 回復性(resilience)をサクッと追加できる 💪✨
.AddStandardResilienceHandler();
builder.Services.AddTransient<CurrencyConverter>();
var app = builder.Build();
AddHttpClientで IHttpClientFactory ベースにできるのがポイント ✨ (Microsoft Learn)AddStandardResilienceHandler()は 標準の回復性パイプライン(リトライ/タイムアウト/遮断など)をまとめて入れてくれます 🔥 (Microsoft for Developers)- しかも標準パイプラインは 429/500系/タイムアウトなどを扱う前提が入ってます(現場で超ありがち)🙏 (Microsoft for Developers)
なお、昔よく使われた
Microsoft.Extensions.Http.Pollyは NuGet 上 “deprecated” 扱いで、今はMicrosoft.Extensions.Http.Resilience系が推奨です。 (NuGet)
7) テスト戦略:単体はFake、結合は少しだけ 🤏🧪✨
7-1) 中(ルール)の単体テスト:Fakeで一瞬 🎉⚡
public sealed class FakeExchangeRateApi : IExchangeRateApi
{
private readonly decimal _rate;
public FakeExchangeRateApi(decimal rate) => _rate = rate;
public Task<decimal> GetUsdToJpyAsync(CancellationToken ct = default)
=> Task.FromResult(_rate);
}
using Xunit;
public class CurrencyConverterTests
{
[Fact]
public async Task ConvertUsdToJpyAsync_UsesRate()
{
var api = new FakeExchangeRateApi(rate: 150m);
var sut = new CurrencyConverter(api);
var jpy = await sut.ConvertUsdToJpyAsync(usd: 2m);
Assert.Equal(300m, jpy);
}
[Fact]
public async Task ConvertUsdToJpyAsync_NegativeUsd_Throws()
{
var api = new FakeExchangeRateApi(rate: 150m);
var sut = new CurrencyConverter(api);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => sut.ConvertUsdToJpyAsync(-1m));
}
}
💖 速い・安定・気持ちいい! 外部APIが落ちててもテストは落ちません✨
7-2) 外(HTTPクライアント)のテスト:HttpMessageHandlerでネット無し 🧪🌐❌

「シリアライズ/URL/ヘッダが合ってる?」みたいな確認は、ネットに出ずにできます😊
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
public sealed class StubHttpMessageHandler : HttpMessageHandler
{
private readonly Funcstring _json;
public StubHttpMessageHandler(string json) => _json = json;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(_json, Encoding.UTF8, "application/json")
};
return Task.FromResult(response);
}
}
using Xunit;
public class ExchangeRateApiClientTests
{
[Fact]
public async Task GetUsdToJpyAsync_ReadsJpyRate()
{
var json = """{"rates":{"JPY":150.0}}""";
var handler = new StubHttpMessageHandler(json);
var http = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.com/")
};
var sut = new ExchangeRateApiClient(http);
var rate = await sut.GetUsdToJpyAsync();
Assert.Equal(150.0m, rate);
}
}
7-3) 結合テスト(本物API)は “少しだけ” 🤏✅
- 「本当に繋がる?」の最終確認は価値あるけど、
- 常時やると不安定&遅い 😵💫
なのでおすすめは👇
- 単体テスト(Fake)を主力にする 🧪⚡
- 本物APIは「最低限の本数」だけ持つ 🤏✨
- 認証キーが要るなら、環境変数などで切り替え(コードに直書きしない)🔑🙅♀️
8) よくある落とし穴まとめ ⚠️😇
落とし穴A:new HttpClient() を毎回やる 🧨
- ソケット枯渇の原因になりがちです (Microsoft Learn)
➡️
IHttpClientFactoryか、長寿命HttpClient戦略で回避が推奨 (Microsoft Learn)
落とし穴B:Factoryで作った HttpClient を長期間キャッシュしちゃう 🧊
IHttpClientFactoryは「短命クライアント前提」の設計思想があり、長期保持はDNS更新などで問題になり得ます (Microsoft Learn) ➡️ Typed client を singleton に突っ込むのも注意 ⚠️ (Microsoft Learn)
落とし穴C:リトライしすぎて相手を殴る 👊😵
- 回復性は大事だけど、無限リトライとかは逆効果 ➡️ 標準パイプラインの考え方(制限/タイムアウト/遮断)を使うと安全寄り 💪 (Microsoft for Developers)
9) AI活用(Copilot/Codex)に頼むと捗るやつ 🤖💡✨
そのままコピって投げてOKなお願い例👇
- 「
IExchangeRateApiの Fake を作って。成功/失敗パターンも欲しい」🎭 - 「
CurrencyConverterの単体テストを xUnit で AAA(Arrange/Act/Assert) で書いて」🧪 - 「HTTPクライアント実装で、DTOとドメイン型が混ざってないかレビューして」🔍
- 「AddHttpClient + AddStandardResilienceHandler の登録例を最小で」🏗️
⚠️ ただしAIは、たまに平気で
- ロジック側にHTTPを混ぜる
- DTOをドメインに漏らす ので、“境界が守れてる?” を最優先でチェックしてね👀✨
10) ミニチェッククイズ ✅🎓
- 重要ロジックの中に
HttpClientを直置きすると何がつらい?😵💫 - 境界インターフェースは「戻り値」を何に寄せるのが気持ちいい?🎯
- 単体テストで外部APIの代わりに何を使う?🎭
IHttpClientFactoryを使う主なメリットは?🧰- 429 が返る外部APIに、やみくもリトライすると何が起きる?💥
まとめ 🎉💖
- 外部APIは 落ちる・遅い・変わる 😵💫
- だから
IExternalApiClient的な境界で包んで、中は純粋ロジックに寄せる 📦🧩 - 単体テストは Fakeで安定&爆速 🧪⚡
- HTTPは
AddHttpClient+(必要なら)標準回復性で堅くする 💪✨ (Microsoft Learn)
次の章(第19章)は、UIもI/Oとして同じ発想で薄くする話に進むよ〜🖥️🚧✨