Next.jsで郵便番号から住所を自動入力する方法
Next.js App Router で、郵便番号から都道府県・市区町村・町域を自動入力する実装手順です。タイムアウト、空結果、レート制限の扱いまで含めたコード例を掲載します。
執筆:住所APIナビ編集部
郵便番号を入力したら住所欄が埋まる、という挙動は入力の手間を大きく減らします。ここでは Next.js App Router で実装する手順を、失敗時の扱いまで含めて説明します。
動作環境
以下の組み合わせを前提に書いています。掲載しているコードは構文チェックを自動で行っていますが、 断片であるため完全な型チェックは通していません。ご自身の環境で確認してください。
- Node.js 24.x
- Next.js 16(App Router)
- TypeScript 5.9(strict)
- React 19
使用するAPIは zipcloud 郵便番号検索API です。APIキーが不要なため、最短で動かせます。本番運用に移す前に、利用規約とレートリミットの扱いを必ず確認してください。
1. 郵便番号を正規化する
先に入力値を整えます。全角数字や 〒、ハイフンの種類がばらつくため、そのまま API に渡すと引けません。
// src/lib/postal-code.ts
/** 入力された郵便番号を7桁の半角数字に整える。整えられなければ null。 */
export function toPostalCode7(input: string): string | null {
const normalized = input
.normalize('NFKC') // 全角数字・全角ハイフンを半角へ
.replace(/[〒\s]/g, '')
.replace(/[-‐‑‒–—―ー]/g, '')
return /^\d{7}$/.test(normalized) ? normalized : null
}NFKC は全角数字 123 を 123 に変換します。ハイフンは種類が多いため、正規化後にまとめて除去しています。
2. Route Handler を作る
// src/app/api/postal-code/route.ts
import { NextResponse } from 'next/server'
import { toPostalCode7 } from '@/lib/postal-code'
type ZipcloudResult = {
zipcode: string
address1: string // 都道府県
address2: string // 市区町村
address3: string // 町域
}
type ZipcloudResponse = {
status: number
message: string | null
results: ZipcloudResult[] | null
}
export async function GET(request: Request) {
const raw = new URL(request.url).searchParams.get('zipcode') ?? ''
const zipcode = toPostalCode7(raw)
if (!zipcode) {
return NextResponse.json(
{ error: 'invalid_zipcode', message: '郵便番号は7桁で入力してください。' },
{ status: 400 },
)
}
// 外部APIが応答しないときにリクエストを溜めないよう、必ず打ち切る。
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch(
`https://zipcloud.ibsnet.co.jp/api/search?zipcode=${zipcode}`,
{ signal: controller.signal, cache: 'no-store' },
)
if (response.status === 429) {
return NextResponse.json(
{ error: 'rate_limited', message: '時間をおいて再度お試しください。' },
{ status: 429 },
)
}
if (!response.ok) {
return NextResponse.json(
{ error: 'upstream_error', message: '住所を取得できませんでした。' },
{ status: 502 },
)
}
const data = (await response.json()) as ZipcloudResponse
// status が 200 でも results が null(該当なし)のことがある。
if (data.status !== 200 || !data.results || data.results.length === 0) {
return NextResponse.json({ candidates: [] }, { status: 200 })
}
return NextResponse.json({
candidates: data.results.map((r) => ({
prefecture: r.address1,
city: r.address2,
town: r.address3,
})),
})
} catch (error) {
const aborted = error instanceof Error && error.name === 'AbortError'
return NextResponse.json(
{
error: aborted ? 'timeout' : 'network_error',
message: '住所を取得できませんでした。手入力でも先に進めます。',
},
{ status: 504 },
)
} finally {
clearTimeout(timeout)
}
}押さえておきたいのは次の4点です。
- タイムアウトを必ず入れる。
AbortControllerがないと、外部APIが遅いときにリクエストが滞留します。 status: 200でもresultsがnullになる。 該当なしはエラーではなく「候補0件」として扱います。- 候補は配列で返す。 郵便番号によっては複数の町域が該当します。1件目を勝手に採用しないでください。
- 失敗しても手入力で先に進める。 住所補完は補助機能です。落ちたらフォーム全体が止まる作りにしてはいけません。
3. フォーム側から呼ぶ
// src/components/AddressFields.tsx
'use client'
import { useState } from 'react'
type Candidate = { prefecture: string; city: string; town: string }
export function AddressFields() {
const [zipcode, setZipcode] = useState('')
const [address, setAddress] = useState('')
const [status, setStatus] = useState<'idle' | 'loading' | 'empty' | 'error'>('idle')
async function lookup(value: string) {
setStatus('loading')
try {
const response = await fetch(`/api/postal-code?zipcode=${encodeURIComponent(value)}`)
const data = (await response.json()) as { candidates?: Candidate[] }
const first = data.candidates?.[0]
if (!first) {
setStatus('empty')
return
}
setAddress(`${first.prefecture}${first.city}${first.town}`)
setStatus('idle')
} catch {
setStatus('error')
}
}
return (
<div>
<label htmlFor="zipcode">郵便番号</label>
<input
id="zipcode"
name="postal-code"
inputMode="numeric"
autoComplete="postal-code"
value={zipcode}
onChange={(event) => setZipcode(event.target.value)}
onBlur={(event) => void lookup(event.target.value)}
/>
<label htmlFor="address">住所</label>
<input
id="address"
name="address-line1"
autoComplete="address-line1"
value={address}
onChange={(event) => setAddress(event.target.value)}
/>
{/* エラーは色だけでなくテキストでも伝える */}
<p role="status" aria-live="polite">
{status === 'loading' && '住所を検索しています…'}
{status === 'empty' && '該当する住所が見つかりませんでした。手入力してください。'}
{status === 'error' && '住所を取得できませんでした。手入力してください。'}
</p>
</div>
)
}実装時の注意点
入力途中で毎回叩かない。 onChange ごとにリクエストすると、7桁入力するだけで最大7回呼ぶことになります。onBlur か、7桁そろった時点で1回だけ呼びます。
番地は自動入力しない。 APIが返すのは町域までです。番地・建物名の欄は空のまま残し、ユーザーに入力してもらいます。
autoComplete を正しく指定する。 postal-code / address-line1 を指定すると、ブラウザの自動入力が効きます。API を呼ぶ前に埋まることも多く、体験としてはこちらのほうが速いです。
複数候補は選ばせる。 同じ郵便番号に複数の町域が対応する場合があります。候補が2件以上あるときは選択UIを出すのが安全です。
表記ゆれは別問題。 APIが返す住所と、ユーザーが手入力した住所は表記が揃いません。手元のデータを整えたい場合は住所整形ツールで挙動を確認できます。
関連ページ
- 日本の郵便番号検索API比較 — 他のAPIとの違い
- zipcloud 郵便番号検索API — 料金・利用条件の詳細
出典
- zipcloud 郵便番号検索API ドキュメント(新しいタブで開く)確認日 2026-07-29
- zipcloud API利用規約(新しいタブで開く)確認日 2026-07-29
- Next.js Route Handlers(新しいタブで開く)確認日 2026-07-29