はじめに
Node.jsでfetch()がグローバルに使えるようになったのは、そう昔の話ではありません。実はこの標準fetch()、内部ではUndiciというHTTPクライアントライブラリが動いています。つまり、意識せずともすでにUndiciのお世話になっている開発者は少なくないはずです。
axiosや標準のhttpモジュールで消耗した経験がある方には特に知ってほしいのですが、Undiciは接続管理やパフォーマンスの面で一段踏み込んだ設計になっています。この記事では、Undiciが何者で、どう使えば真価を発揮するのかを見ていきます。
Undiciとは
UndiciはNode.js公式チームが開発しているHTTPクライアントライブラリです。Node.jsコア開発者でもあるMatteo Collina氏らが中心となって開発しており、Node.js 18以降で標準搭載されているfetch()・Headers・Request・Response・WebSocket・EventSourceの実装基盤そのものを提供しています。名前の由来はイタリア語で「11」を意味する言葉で、HTTP/1.1へのオマージュだそうです。
主な特徴
- 標準fetch()の実装基盤 - Node.js組み込みの
fetch()はUndiciのAPIをラップしたものなので、標準APIのまま高速な通信が手に入ります - コネクションプーリングとパイプライン処理 -
PoolやClientを使えば、TCP接続を再利用しながら大量のリクエストを効率よくさばけます - Interceptors(インターセプター) - リトライ、リダイレクト、レスポンスキャッシュなどの横断的な処理を、パイプ形式で自由に組み合わせられます
- MockAgentによるテスト支援 - 実際のネットワークに接続せずにHTTPリクエストをモックでき、単体テストが書きやすくなります
- 標準ベースの設計 - WHATWG Fetch仕様やWebSocket仕様に準拠しているため、ブラウザのAPIと同じ感覚で扱えます
インストール
Undici自体はNode.js 18以降にバンドルされているため、標準fetch()を使うだけならインストール不要です。より新しいバージョンの機能や、Pool・Agent・MockAgentなどの高度なAPIを明示的に使いたい場合は、パッケージとして追加できます。
npm install undici
yarn add undici
pnpm add undici
基本的な使い方
UndiciはNode.js専用のライブラリであり、ブラウザ上では動作しません。以下のサンプルはNode.js環境で実行することを前提にしています。
最もシンプルな使い方は、標準のfetch()と同じ感覚で使えるrequest()メソッドです。
import { request } from 'undici'
const { statusCode, body } = await request('https://httpbin.org/get')
console.log('status:', statusCode)
const data = await body.json()
console.log(data)
fetch()との違いは、レスポンスボディがReadableStreamではなくNode.jsのReadableストリームとして扱える点です。body.json()・body.text()・body.arrayBuffer()といったヘルパーで、必要な形式に簡単に変換できます。
実践的なユースケース
大量リクエストをさばく - Poolでコネクションを再利用する
同じホストに何百・何千とリクエストを送る場合、毎回TCPコネクションを張り直すのは非効率です。Poolを使うと、指定した数のコネクションを維持しながらリクエストをキューイングし、効率よく使い回してくれます。
import { Pool } from 'undici'
const pool = new Pool('https://httpbin.org', {
connections: 10,
})
async function fetchMany(count) {
const requests = Array.from({ length: count }, (_, i) =>
pool.request({ path: `/get?id=${i}`, method: 'GET' })
)
const results = await Promise.all(requests)
console.log(`${results.length}件のリクエストが完了しました`)
await pool.close()
}
fetchMany(20)
通信の信頼性を上げる - Interceptorsでリトライを組み込む
外部APIとの通信では、一時的なネットワークエラーやタイムアウトがつきものです。interceptors.retry()を使えば、失敗したリクエストを自動的に再試行するロジックを、呼び出し側のコードを汚さずに差し込めます。
import { Agent, interceptors, request } from 'undici'
const agent = new Agent().compose(
interceptors.retry({
maxRetries: 3,
minTimeout: 500,
methods: ['GET'],
})
)
const { statusCode, body } = await request('https://httpbin.org/status/500', {
dispatcher: agent,
})
console.log('最終ステータス:', statusCode)
await body.text()
外部通信なしでテストする - MockAgentでモックする
APIクライアントの単体テストで実際のネットワークに接続してしまうと、テストが不安定になったり実行時間が伸びたりします。MockAgentを使えば、任意のエンドポイントへのリクエストをインターセプトし、決まったレスポンスを返せます。
import { MockAgent, setGlobalDispatcher, request } from 'undici'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('https://api.example.com')
mockPool
.intercept({ path: '/users/1', method: 'GET' })
.reply(200, { id: 1, name: 'Taro' })
const { statusCode, body } = await request('https://api.example.com/users/1')
const data = await body.json()
console.log(statusCode, data)
まとめ
Undiciは、Node.jsの標準fetch()を裏側から支えているだけでなく、Poolによるコネクション管理、Interceptorsによる柔軟な拡張、MockAgentによるテスト容易性まで、実務で求められる機能を一通り備えたHTTPクライアントです。標準APIをそのまま使うだけでも十分速く、さらに踏み込めば高負荷な通信処理も手なずけられます。「なんとなくaxiosを使っている」という方は、一度Undiciに置き換えてみると、その速度と設計の違いを体感できるはずです。
