JavaScript Base64 デコード完全ガイド — atob() と Buffer

·Front-end & Node.js Developer·レビュー担当Sophie Laurent·公開日

無料の Base64デコーダー をブラウザで直接使用 — インストール不要。

Base64デコーダー をオンラインで試す →

本番環境の認証問題をデバッグするとき、私が最初に手を伸ばすのは Base64 デコーダーです — JWT ペイロード、Webhook 署名、エンコードされた設定値はすべて Base64 文字列の中に隠れています。 JavaScript には Base64 デコードの主要な組み込みアプローチが 2 つあります: atob()(ブラウザ + Node.js 16+)と Buffer.from(encoded, 'base64').toString() (Node.js)— そして元データに Unicode 文字が含まれている場合、両者の動作は大きく異なります。 コードを書かずにすぐにデコードしたい場合は、 ToolDeck's Base64 Decoder がブラウザで即座に処理します。本ガイドでは両環境を対象に — Node.js 16+ と最新ブラウザ(Chrome 80+、Firefox 75+、Safari 14+)— 本番環境で使えるサンプルを紹介します:UTF-8 復元、URL セーフバリアント、JWT デコード、ファイル、 API レスポンス、Node.js ストリーム、そして実際のコードベースで文字化けを引き起こす 4 つのよくある間違いです。

  • atob(encoded) はブラウザネイティブで Node.js 16+ ではグローバルに使用できますが、バイナリ文字列を返します — ASCII を超えるコンテンツには TextDecoder を使って UTF-8 テキストを復元してください。
  • Buffer.from(encoded, "base64").toString("utf8") は Node.js の慣用的なアプローチで、追加手順なしに UTF-8 を自動的に処理します。
  • URL セーフ Base64(JWT で使用)は + を - に、/ を _ に置き換え、= パディングを除去します。atob() を呼び出す前にこれらを復元するか、Node.js 18+ では Buffer.from(encoded, "base64url").toString() を使用してください。
  • デコード前に空白と改行を除去してください — GitHub Contents API や多くの MIME エンコーダーは Base64 出力を 1 行 60〜76 文字で折り返します。
  • Uint8Array.prototype.fromBase64()(TC39 Stage 3)はすでに Node.js 22+ と Chrome 130+ で利用可能で、将来的に両環境を統一します。

Base64 デコードとは?

Base64 デコードはエンコードの逆操作です — 64 文字の ASCII 表現を元のバイナリデータやテキストに変換します。 4 つの Base64 文字はちょうど 3 バイトにマッピングされます。 エンコードされた文字列の末尾にある = パディング文字は、最後の 3 バイトグループを完成させるために何バイト追加されたかをデコーダーに伝えます。

Base64 は暗号化ではありません — エンコードされた文字列を持つ誰でも完全に逆操作できます。 その目的はトランスポートの安全性です:7 ビット ASCII テキスト用に設計されたプロトコルやストレージ形式は 任意のバイナリバイトを処理できず、Base64 がそのギャップを埋めます。 JavaScript でよくあるデコードシナリオには、JWT ペイロードの検査、環境変数からの Base64 エンコードされた JSON 設定の展開、REST API からのバイナリファイルコンテンツの抽出、 ブラウザでの data URI のデコードなどがあります。

Before · text
After · text
ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=
deploy-bot:sk-prod-a7f2c91e4b3d8

atob() — ブラウザネイティブのデコード関数

atob()(ASCII-to-binary)は IE10 からブラウザで使用可能で、 WinterCG 互換性イニシアチブの一環として Node.js 16.0 でグローバル関数になりました。 Deno、Bun、Cloudflare Workers でもネイティブに動作し、インポートは不要です。

この関数はバイナリ文字列を返します:各文字のコードポイントが 1 つの生のバイト値(0–255)に等しい JavaScript 文字列です。これは重要です:元のデータが U+007F 以上の文字(アクセント付き文字、キリル文字、CJK、絵文字)を 含む UTF-8 テキストだった場合、返される文字列は読めるテキストではなく生のバイト列です。 復元するには TextDecoder を使います(次のセクションで説明)。

最小限の動作例

JavaScript (browser / Node.js 16+)
// Decoding an HTTP Basic Auth credential pair received in a request header
// Authorization: Basic ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=

function parseBasicAuth(header: string): { serviceId: string; apiKey: string } {
  const base64Part = header.replace(/^Basics+/i, '')
  const decoded    = atob(base64Part)
  const [serviceId, apiKey] = decoded.split(':')
  return { serviceId, apiKey }
}

const auth = parseBasicAuth('Basic ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=')

console.log(auth.serviceId) // deploy-bot
console.log(auth.apiKey)    // sk-prod-a7f2c91e4b3d8

ラウンドトリップ検証

JavaScript
// Verify lossless recovery for ASCII-only content
const original = 'service:payments region:eu-west-1 env:production'

const encoded = btoa(original)
const decoded = atob(encoded)

console.log(encoded)
// c2VydmljZTpwYXltZW50cyByZWdpb246ZXUtd2VzdC0xIGVudjpwcm9kdWN0aW9u

console.log(decoded === original) // true
注意:atob()btoa() WinterCG Minimum Common API の一部です — 非ブラウザランタイムでの Fetch、URL、crypto を管理する同じ仕様です。 Node.js 16+、Bun、Deno、Cloudflare Workers で同一に動作します。

デコード後の UTF-8 テキストの復元

atob() の最もよくある落とし穴は、 その戻り値の型を誤解することです。元のテキストが Base64 エンコード前に UTF-8 でエンコードされていた場合、atob() は読めるテキストではなく Latin-1 バイナリ文字列を返します:

JavaScript
// '田中太郎' was UTF-8 encoded then Base64 encoded before transmission
const encoded = '55Sw5Lit5aSq6YOO'

// ❌ atob() returns the raw UTF-8 bytes as a Latin-1 string — garbled output
console.log(atob(encoded))
// "ç"°ä¸­å¤ªé ã"  ← byte values misread as Latin-1

正しいアプローチは TextDecoder を使って生のバイトを UTF-8 として解釈することです:

TextDecoder アプローチ — あらゆる Unicode 出力に対応

JavaScript (browser + Node.js 16+)
// Unicode-safe Base64 decode utilities
function fromBase64(encoded: string): string {
  const binary = atob(encoded)
  const bytes  = Uint8Array.from(binary, ch => ch.charCodeAt(0))
  return new TextDecoder().decode(bytes)
}

function toBase64(text: string): string {
  const bytes = new TextEncoder().encode(text)
  const chars = Array.from(bytes, byte => String.fromCharCode(byte))
  return btoa(chars.join(''))
}

// Works with any language or script
const orderNote = '確認済み:田中太郎 — 東京倉庫、数量:250'
const encoded   = toBase64(orderNote)
const decoded   = fromBase64(encoded)

console.log(decoded === orderNote) // true
console.log(decoded)
// 確認済み:田中太郎 — 東京倉庫、数量:250
注意:Node.js では TextDecoder のステップを完全にスキップできます — Buffer.from(encoded, 'base64').toString('utf8') を使ってください。 デコードされたバイトを自動的に UTF-8 として解釈し、大きな入力では高速です。

Node.js の Buffer.from() — 完全デコードガイド

Node.js では、Buffer が Base64 デコードを含む すべてのバイナリ操作の慣用的な API です。UTF-8 をネイティブに処理し、 真の Buffer(バイナリセーフ)を返し、 Node.js 18 以降は URL セーフバリアントの 'base64url' エンコーディングショートカットをサポートします。

環境変数の設定をデコードする

Node.js
// Server config stored as Base64 in an env variable (avoids JSON escaping in shell)
// DB_CONFIG=eyJob3N0IjoiZGItcHJpbWFyeS5pbnRlcm5hbCIsInBvcnQiOjU0MzIsImRhdGFiYXNlIjoiYW5hbHl0aWNzX3Byb2QiLCJtYXhDb25uZWN0aW9ucyI6MTAwfQ==

const raw = Buffer.from(process.env.DB_CONFIG!, 'base64').toString('utf8')
const dbConfig = JSON.parse(raw)

console.log(dbConfig.host)           // db-primary.internal
console.log(dbConfig.port)           // 5432
console.log(dbConfig.maxConnections) // 100

.b64 ファイルからバイナリファイルを復元する

Node.js
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'

// Read the Base64-encoded certificate and restore the original binary
const encoded = readFileSync(join(process.cwd(), 'dist', 'cert.b64'), 'utf8').trim()
const certBuf  = Buffer.from(encoded, 'base64')

writeFileSync('./ssl/server.crt', certBuf)

console.log(`Restored ${certBuf.length} bytes`)
// Restored 2142 bytes

エラーハンドリング付き非同期デコード

Node.js
import { readFile, writeFile } from 'node:fs/promises'

async function decodeBase64File(
  encodedPath: string,
  outputPath:  string,
): Promise<number> {
  try {
    const encoded = await readFile(encodedPath, 'utf8')
    const binary  = Buffer.from(encoded.trim(), 'base64')
    await writeFile(outputPath, binary)
    return binary.length
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code
    if (code === 'ENOENT') throw new Error(`File not found: ${encodedPath}`)
    if (code === 'EACCES') throw new Error(`Permission denied: ${encodedPath}`)
    throw err
  }
}

// Restore a PDF stored as Base64
const bytes = await decodeBase64File('./uploads/invoice.b64', './out/invoice.pdf')
console.log(`Decoded ${bytes} bytes — PDF restored`)

Base64 デコード関数 — パラメータリファレンス

コードを書いたりレビューしたりする際に参照できる、2 つの主要なネイティブデコード API のパラメータクイックリファレンスです。

atob(encodedData)

パラメータ必須説明
encodedDatastringはい+、/、= 文字を使った標準 Base64 文字列。URL セーフバリアント(-、_)は InvalidCharacterError をスローします。空白は許可されません。
返り値:バイナリ文字列 — 各文字のコードポイントが 1 つの生のバイト値(0–255)に等しい。Unicode 文字列ではないため、UTF-8 テキストを復元するには TextDecoder を通す必要があります。

Buffer.from(input, inputEncoding) / .toString(outputEncoding)

パラメータデフォルト説明
inputstring | Buffer | TypedArray | ArrayBuffer必須デコードする Base64 エンコードされた文字列、またはエンコードされたバイトを含む Buffer。
inputEncodingBufferEncoding"utf8"標準 Base64(RFC 4648 §4)は "base64"、URL セーフ Base64(RFC 4648 §5、Node.js 18+)は "base64url" を指定。
outputEncodingstring"utf8".toString() 出力のエンコーディング。読めるテキストには "utf8"、atob() 出力と互換の Latin-1 バイナリ文字列には "binary" を使用。
startinteger0デコードされた Buffer 内で読み込みを開始するバイトオフセット。.toString() の第 2 引数として渡される。
endintegerbuf.length読み込みを停止するバイトオフセット(排他)。.toString() の第 3 引数として渡される。
返り値:.from() は Buffer を返します。.toString() は文字列を返します。デコードされたコンテンツがバイナリ(画像、PDF、音声)の場合は Buffer のまま保持してください(.toString() を呼び出さない)— writeFile またはレスポンスストリームに直接渡します。

URL セーフ Base64 — JWT と URL パラメータのデコード

JWT はすべての 3 つのセグメントに URL セーフ Base64(RFC 4648 §5)を使用します。 URL セーフ Base64 は + - に、 / _ に置き換え、末尾の = パディングを除去します。 復元せずにこれを atob() に渡すと、 誤った出力が生成されるかスローされます。

ブラウザ — デコード前に文字とパディングを復元する

JavaScript (browser)
function decodeBase64Url(input: string): string {
  const base64 = input.replace(/-/g, '+').replace(/_/g, '/')
  const padded = base64 + '==='.slice(0, (4 - base64.length % 4) % 4)
  const binary = atob(padded)
  const bytes  = Uint8Array.from(binary, ch => ch.charCodeAt(0))
  return new TextDecoder().decode(bytes)
}

// Inspect a JWT payload segment (the middle part between the two dots)
const jwtToken  = 'eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsIndvcmtzcGFjZUlkIjoid3NfM2E3ZjkxYzIiLCJleHAiOjE3MTcyMDM2MDB9'
const payload   = decodeBase64Url(jwtToken)
const claims    = JSON.parse(payload)

console.log(claims.userId)      // usr_9f2a1c3e8b4d
console.log(claims.role)        // editor
console.log(claims.workspaceId) // ws_3a7f91c2

Node.js 18+ — ネイティブ 'base64url' エンコーディング

Node.js 18+
// Node.js 18 added 'base64url' as a first-class Buffer encoding — no manual replace needed
function decodeJwtSegment(segment: string): Record<string, unknown> {
  const json = Buffer.from(segment, 'base64url').toString('utf8')
  return JSON.parse(json)
}

const token   = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsIndvcmtzcGFjZUlkIjoid3NfM2E3ZjkxYzIiLCJleHAiOjE3MTcyMDM2MDB9.SIGNATURE'
const [headerB64, payloadB64] = token.split('.')

const header  = decodeJwtSegment(headerB64)
const payload = decodeJwtSegment(payloadB64)

console.log(header.alg)          // HS256
console.log(payload.role)        // editor
console.log(payload.workspaceId) // ws_3a7f91c2

ファイルと API レスポンスからの Base64 デコード

本番コードでは、Base64 デコードはエンコードされた形式でコンテンツを提供する外部 API を 消費するときに最も頻繁に発生します。両シナリオとも空白の扱いとバイナリ vs テキスト出力に関して 重要な注意点があります。デバッグ中にエンコードされたレスポンスを確認するだけなら、 直接 Base64 Decoder に貼り付けてください — 標準と URL セーフモードを即座に処理します。

GitHub Contents API からのコンテンツをデコードする

JavaScript
// GitHub Contents API returns file content as Base64, wrapped at 60 chars per line
async function fetchDecodedFile(
  owner: string,
  repo:  string,
  path:  string,
  token: string,
): Promise<string> {
  const res = await fetch(
    `https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
    { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json' } }
  )
  if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`)

  const data = await res.json() as { content: string; encoding: string }
  if (data.encoding !== 'base64') throw new Error(`Unexpected encoding: ${data.encoding}`)

  // ⚠️ GitHub wraps at 60 chars — strip newlines before decoding
  const clean = data.content.replace(/\n/g, '')
  return Buffer.from(clean, 'base64').toString('utf8')
}

const openApiSpec = await fetchDecodedFile('acme-corp', 'platform-api', 'openapi.json', process.env.GITHUB_TOKEN!)
const spec = JSON.parse(openApiSpec)
console.log(`API version: ${spec.info.version}`)

API からの Base64 エンコードされたバイナリをデコードする(ブラウザ)

JavaScript (browser)
// Some APIs return binary content (images, PDFs) as Base64 JSON fields
async function downloadDecodedFile(endpoint: string, authToken: string): Promise<void> {
  const res = await fetch(endpoint, { headers: { Authorization: `Bearer ${authToken}` } })
  if (!res.ok) throw new Error(`Download failed: ${res.status}`)

  const { filename, content, mimeType } = await res.json() as {
    filename: string; content: string; mimeType: string
  }

  // Decode Base64 → binary bytes → Blob
  const binary = atob(content)
  const bytes  = Uint8Array.from(binary, ch => ch.charCodeAt(0))
  const blob   = new Blob([bytes], { type: mimeType })

  // Trigger browser download
  const url = URL.createObjectURL(blob)
  const a   = Object.assign(document.createElement('a'), { href: url, download: filename })
  a.click()
  URL.revokeObjectURL(url)
}

await downloadDecodedFile('/api/reports/latest', sessionStorage.getItem('auth_token')!)

Node.js とシェルでのコマンドライン Base64 デコード

CI/CD スクリプト、デバッグセッション、または一度限りのデコードタスクには、 シェルツールや Node.js ワンライナーの方が完全なスクリプトより速いです。 macOS と Linux でフラグ名が異なることに注意してください。

bash
# ── macOS / Linux system base64 ───────────────────────────────────────
# Standard decoding (macOS uses -D, Linux uses -d)
echo "ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=" | base64 -d   # Linux
echo "ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=" | base64 -D   # macOS

# Decode a .b64 file to its original binary
base64 -d ./dist/cert.b64 > ./ssl/server.crt       # Linux
base64 -D -i ./dist/cert.b64 -o ./ssl/server.crt   # macOS

# URL-safe Base64 — restore + and / before decoding
echo "eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ" | tr '-_' '+/' | base64 -d

# ── Node.js one-liner — works on Windows too ───────────────────────────
node -e "process.stdout.write(Buffer.from(process.argv[1], 'base64').toString())" "ZGVwbG95LWJvdA=="
# deploy-bot

# URL-safe (Node.js 18+)
node -e "process.stdout.write(Buffer.from(process.argv[1], 'base64url').toString())" "eyJhbGciOiJIUzI1NiJ9"
# {"alg":"HS256"}
注意:macOS では base64 のデコードに -D(大文字)を使いますが、Linux は -d(小文字)を使います。 これにより CI スクリプトがサイレントに失敗します — ターゲットプラットフォームが Linux である保証がない場合は Node.js ワンライナーを使用してください。

高性能な代替手段:js-base64

ライブラリを選ぶ主な理由はクロス環境の一貫性です。ブラウザと Node.js の両方で バンドラー設定なしに動作するパッケージを公開する場合、Buffer は環境検出が必要で、atob() は TextDecoder の回避策が必要です。js-base64(npm 週間ダウンロード数 1 億以上)は 両方を透過的に処理します。

bash
npm install js-base64
# or
pnpm add js-base64
JavaScript
import { fromBase64, fromBase64Url, isValid } from 'js-base64'

// Standard decoding — Unicode-safe, works in browser and Node.js
const raw   = fromBase64('eyJldmVudElkIjoiZXZ0XzdjM2E5ZjFiMmQiLCJ0eXBlIjoiY2hlY2tvdXRfY29tcGxldGVkIiwiY3VycmVuY3kiOiJFVVIiLCJhbW91bnQiOjE0OTAwfQ==')
const event = JSON.parse(raw)
console.log(event.type)     // checkout_completed
console.log(event.currency) // EUR

// URL-safe decoding — no manual character replacement needed
const jwtPayload = fromBase64Url('eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciJ9')
const claims     = JSON.parse(jwtPayload)
console.log(claims.role) // editor

// Validate before decoding untrusted input
const untrusted = 'not!valid@base64#'
if (!isValid(untrusted)) {
  console.error('Rejected: invalid Base64 input')
}

// Binary output — second argument true returns Uint8Array
const pngBytes = fromBase64('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', true)
console.log(pngBytes instanceof Uint8Array) // true

シンタックスハイライト付きターミナル出力

CLI デバッグツールや検査スクリプトを書くとき、大きな JSON ペイロードに対して プレーンな console.log 出力は読みにくいです。chalk(ターミナル着色で最もダウンロードされる npm パッケージ)と Base64 デコードを組み合わせると、読みやすくスキャンしやすいターミナル出力が得られます — JWT 検査、API レスポンスのデバッグ、設定監査に役立ちます。

bash
npm install chalk
# chalk v5+ is ESM-only — use import, not require
Node.js
import chalk from 'chalk'

// Decode and display any Base64 value with smart type detection
function inspectBase64(encoded: string, label = 'Decoded value'): void {
  let decoded: string
  try {
    decoded = Buffer.from(encoded.trim(), 'base64').toString('utf8')
  } catch {
    console.error(chalk.red('✗ Invalid Base64 input'))
    return
  }

  console.log(chalk.bold.cyan(`\n── ${label} ──`))

  // Attempt JSON pretty-print
  try {
    const parsed = JSON.parse(decoded)
    console.log(chalk.green('Type:'), chalk.yellow('JSON'))
    for (const [key, value] of Object.entries(parsed)) {
      const display = typeof value === 'object' ? JSON.stringify(value) : String(value)
      console.log(chalk.green(`  ${key}:`), chalk.white(display))
    }
    return
  } catch { /* not JSON */ }

  // Plain text fallback
  console.log(chalk.green('Type:'), chalk.yellow('text'))
  console.log(chalk.white(decoded))
}

// Inspect a Base64-encoded JWT payload
const tokenParts = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsImV4cCI6MTcxNzIwMzYwMH0.SIGNATURE'.split('.')
inspectBase64(tokenParts[0], 'JWT Header')
inspectBase64(tokenParts[1], 'JWT Payload')
// ── JWT Header ──
// Type:   JSON
//   alg:  HS256
//   typ:  JWT
//
// ── JWT Payload ──
// Type:   JSON
//   userId: usr_9f2a1c3e8b4d
//   role:   editor
//   exp:    1717203600
注意:chalk はターミナル/CLI 出力専用です — ファイル、API レスポンス、ログアグリゲーターに書き込むコンテンツには絶対に使わないでください。 ANSI エスケープコードは非ターミナルのコンシューマーを破壊します:ログプラットフォーム(Datadog、Splunk)、 JSON ログパーサー、CI ログビューアーはすべて判読不能な文字列として表示します。

Node.js ストリームを使った大容量 Base64 ファイルのデコード

Base64 エンコードされたファイルが約 50 MB を超える場合、readFileSync() を使ってメモリ全体に読み込むと問題が生じます。 Node.js ストリームを使うとデータをチャンクでデコードできますが — Base64 はチャンク境界でのパディングエラーを避けるために、 チャンクごとに 4 文字の倍数が必要です(4 文字グループがちょうど 3 バイトにデコードされます)。

Node.js
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'

async function streamDecodeBase64(inputPath: string, outputPath: string): Promise<void> {
  const readStream  = createReadStream(inputPath, { encoding: 'utf8', highWaterMark: 4 * 1024 * 192 })
  const writeStream = createWriteStream(outputPath)

  let buffer = ''

  await pipeline(
    readStream,
    async function* (source) {
      for await (const chunk of source) {
        buffer += (chunk as string).replace(/\s/g, '') // strip any whitespace/newlines

        // Decode only complete 4-char groups to avoid mid-stream padding issues
        const remainder = buffer.length % 4
        const safe      = buffer.slice(0, buffer.length - remainder)
        buffer          = buffer.slice(buffer.length - remainder)

        if (safe.length > 0) yield Buffer.from(safe, 'base64')
      }
      if (buffer.length > 0) yield Buffer.from(buffer, 'base64')
    },
    writeStream,
  )
}

// Decode a 200 MB video that was stored as Base64
await streamDecodeBase64('./uploads/product-demo.b64', './dist/product-demo.mp4')
console.log('Stream decode complete')
注意:Base64 テキストを読み込む際、チャンクサイズは 4 文字の倍数でなければなりません。 これにより各チャンクに完全な 4 文字グループだけが含まれます。 サンプルでは 4 × 1024 × 192 = 786,432 文字(768 KB)を使用しています。 50 MB 未満のファイルには、 readFile() + Buffer.from(content.trim(), 'base64') の方がシンプルで十分高速です。

よくある間違い

以下の 4 つの間違いは JavaScript コードベースで繰り返し見られます — 非 ASCII 文字や行折り返しされた API レスポンスが本番環境のデコードパスに到達するまで 隠れていることが多いです。

間違い 1 — UTF-8 コンテンツに TextDecoder なしで atob() を使う

問題: atob() は各文字が 1 つの生のバイト値を表すバイナリ文字列を返します。 UTF-8 マルチバイトシーケンス(キリル文字、CJK、アクセント付き文字)は 文字化けした Latin-1 文字として表示されます。 修正: 出力を TextDecoder でラップします。

Before · JavaScript
After · JavaScript
// ❌ atob() returns the raw UTF-8 bytes as a Latin-1 string
const encoded = '0JDQu9C10LrRgdC10Lkg0JjQstCw0L3QvtCy'
const decoded  = atob(encoded)
console.log(decoded)
// "Алексей Р˜РІР°РЅРѕРІ"  ← wrong
// ✅ Use TextDecoder to correctly interpret the UTF-8 bytes
const encoded  = '0JDQu9C10LrRgdC10Lkg0JjQstCw0L3QvtCy'
const binary   = atob(encoded)
const bytes    = Uint8Array.from(binary, ch => ch.charCodeAt(0))
const decoded  = new TextDecoder().decode(bytes)
console.log(decoded) // Алексей Иванов ✓

間違い 2 — URL セーフ Base64 を直接 atob() に渡す

問題: JWT セグメントは + / の代わりに - _ を使い、パディングがありません。atob() は誤った結果を返すかスローする可能性があります。 修正: 先に標準文字を復元してパディングを追加します。

Before · JavaScript
After · JavaScript
// ❌ URL-safe JWT segment passed directly — unreliable
const jwtPayload = 'eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ'
const decoded    = atob(jwtPayload) // May produce wrong result or throw
// ✅ Restore standard Base64 chars and padding first
function decodeBase64Url(input: string): string {
  const b64  = input.replace(/-/g, '+').replace(/_/g, '/')
  const pad  = b64 + '==='.slice(0, (4 - b64.length % 4) % 4)
  const bin  = atob(pad)
  const bytes = Uint8Array.from(bin, ch => ch.charCodeAt(0))
  return new TextDecoder().decode(bytes)
}
const decoded = decodeBase64Url('eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ')
// {"userId":"usr_9f2a1c3e"} ✓

間違い 3 — 行折り返しされた Base64 の改行を除去しない

問題: GitHub Contents API と MIME エンコーダーは Base64 出力を 1 行 60〜76 文字で折り返します。atob() \n 文字で InvalidCharacterError をスローします。 修正: デコード前にすべての空白を除去します。

Before · JavaScript
After · JavaScript
// ❌ GitHub API content field contains embedded newlines
const data    = await res.json()
const decoded = atob(data.content) // ❌ throws InvalidCharacterError
// ✅ Strip newlines (and any other whitespace) before decoding
const data    = await res.json()
const clean   = data.content.replace(/\s/g, '')
const decoded = atob(clean) // ✓

間違い 4 — デコードされたバイナリコンテンツに .toString() を呼び出す

問題: 元のデータがバイナリ(画像、PDF、音声)の場合、.toString('utf8') を呼び出すと 認識できないバイトシーケンスを U+FFFD に置き換え、 出力をサイレントに破損させます。 修正: 結果を Buffer として保持します — 文字列に変換しないでください。

Before · JavaScript
After · JavaScript
// ❌ .toString('utf8') corrupts binary content
import { readFileSync, writeFileSync } from 'node:fs'
const encoded   = readFileSync('./uploads/invoice.b64', 'utf8').trim()
const corrupted = Buffer.from(encoded, 'base64').toString('utf8') // ❌
writeFileSync('./out/invoice.pdf', corrupted) // ❌ unreadable PDF
// ✅ Keep the Buffer as binary — do not convert to a string
import { readFileSync, writeFileSync } from 'node:fs'
const encoded = readFileSync('./uploads/invoice.b64', 'utf8').trim()
const binary  = Buffer.from(encoded, 'base64') // ✓ raw bytes preserved
writeFileSync('./out/invoice.pdf', binary)      // ✓ valid PDF

JavaScript Base64 デコードメソッド — クイック比較

メソッドUTF-8 出力バイナリ出力URL セーフ対応環境インストール
atob()❌ TextDecoder が必要✅ バイナリ文字列❌ 手動で復元Browser, Node 16+, Bun, Deno不要
TextDecoder + atob()✅ UTF-8✅ Uint8Array 経由❌ 手動で復元Browser, Node 16+, Deno不要
Buffer.from().toString()✅ utf8✅ Buffer として保持✅ base64url (Node 18+)Node.js, Bun不要
Uint8Array.fromBase64() (TC39)✅ TextDecoder 経由✅ ネイティブ✅ alphabet オプションChrome 130+, Node 22+不要
js-base64✅ 常に✅ Uint8Array✅ 組み込み全環境npm install

デコードされるコンテンツが ASCII テキストであることが保証されている場合のみ atob() を選択してください。 ブラウザでのユーザー入力や多言語テキストには TextDecoder + atob() を使います。 Node.js サーバーサイドコードには、Buffer が正しいデフォルトです — UTF-8 を自動処理し、バイナリデータをそのまま保持します。 クロス環境ライブラリには、 js-base64 がすべてのエッジケースを解消します。

よくある質問

atob() が読めるテキストではなく文字化けを返すのはなぜですか?
atob() は各文字が 1 つの生のバイト(0–255)を表すバイナリ文字列を返します。Unicode コードポイントではありません。元のテキストが UTF-8 でエンコードされていた場合、U+007F 以上の文字——キリル文字、アラビア文字、CJK 表意文字、アクセント付き文字——はすべて 2 つ以上の文字化けした Latin-1 文字として表示されます。修正方法:出力を TextDecoder に通します:const bytes = Uint8Array.from(atob(encoded), ch => ch.charCodeAt(0)); const text = new TextDecoder().decode(bytes)。Node.js では Buffer.from(encoded, 'base64').toString('utf8') を使うと自動的に処理されます。
JavaScript で JWT トークンのペイロードをデコードするにはどうすればよいですか?
JWT はドットで区切られた 3 つの URL セーフ Base64 セグメントで構成されています:header.payload.signature。ペイロードをデコードするには:const [, payloadB64] = token.split('.')。ブラウザでは:標準文字を復元し、パディングを追加し、atob() と TextDecoder でデコードします。Node.js 18+ では:Buffer.from(payloadB64, 'base64url').toString('utf8')。重要:デコードはクレームを明らかにするだけで、署名を検証しません。本番環境での検証済みデコードには適切な JWT ライブラリ(jsonwebtoken、jose)を使用してください。
デコードにおける atob() と Buffer.from() の違いは何ですか?
atob() はすべての JavaScript 環境(ブラウザ、Node.js 16+、Bun、Deno)でインポートなしに使えますが、バイナリ文字列を返します — UTF-8 コンテンツを読めるテキストに変換するには TextDecoder が必要です。Buffer.from(encoded, 'base64') は Node.js / Bun 専用で、実際の Buffer(バイナリセーフ)を返し、UTF-8 をネイティブに処理し、Node.js 18+ では 'base64url' をサポートします。サーバーサイドコードには Buffer がシンプルです。ブラウザコードには atob() + TextDecoder が標準です。クロス環境ライブラリには js-base64 が違いを抽象化します。
ブラウザで URL セーフ Base64 をデコードするにはどうすればよいですか?
URL セーフ Base64 は + を - に、/ を _ に置き換え、= パディングを除去します。atob() を呼び出す前に復元します:const b64 = input.replace(/-/g, '+').replace(/_/g, '/'); const padded = b64 + '==='.slice(0, (4 - b64.length % 4) % 4); const text = new TextDecoder().decode(Uint8Array.from(atob(padded), c => c.charCodeAt(0)))。Node.js 18+ では:Buffer.from(input, 'base64url').toString('utf8') で 1 回の呼び出しで処理できます。
JavaScript で GitHub API からの Base64 コンテンツをデコードするにはどうすればよいですか?
GitHub Contents API は標準 Base64 でファイルコンテンツを返しますが、60 文字ごとに改行文字が入ります。デコード前に除去します:const clean = data.content.replace(/\n/g, '')。ブラウザでは:new TextDecoder().decode(Uint8Array.from(atob(clean), c => c.charCodeAt(0)))。Node.js では:Buffer.from(clean, 'base64').toString('utf8')。バイナリファイル(画像、PDF)の場合は .toString() を呼び出さず Buffer を保持します — writeFile またはレスポンスストリームに直接渡します。
ライブラリなしでブラウザで Base64 エンコードされた画像をデコードできますか?
はい。ブラウザでは:const binary = atob(encoded); const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0)); const blob = new Blob([bytes], { type: 'image/png' }); const url = URL.createObjectURL(blob)。img src の場合は data URI を構築する方がシンプルです:const src = 'data:image/png;base64,' + encoded — これはデコードステップを完全にスキップします。Node.js では:Buffer.from(encoded, 'base64') の後に writeFileSync('./out.png', buffer)。重要なルール:コンテンツがバイナリの場合、デコードされた Buffer に絶対 .toString() を呼び出さないでください。

関連ツール

コードを書かずにワンクリックでデコードするには、Base64 文字列を直接 Base64 Decoder に貼り付けてください — ブラウザで標準と URL セーフモードを即座に処理します。

他の言語でも利用可能:PythonGoJavaC#
AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

SL
Sophie Laurent技術レビュアー

Sophie is a full-stack developer focused on TypeScript across the entire stack — from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.