🎬

UUIDを短くするライブラリを作った

に公開
4

はじめに

WebサービスやAPIでよく使われるUUIDは、一意性が高い反面、36文字と長くてURLに埋め込むと見た目が悪くなりがちです。

例えば、

https://example.com/resource/f4b247fd-1f87-45d4-aa06-1c6fc0a8dfaf

のようなURLは冗長で、コピーや手入力もしづらいです。

そこで、UUIDをBase58(Bitcoinでも使われる、視認性が高くURLセーフな文字セット)でエンコードすると、

https://example.com/resource/XDY9dmBbcMBXqcRvYw8xJ2

のように、22文字の短くてスッキリしたIDに変換できます。

これを簡単に行うためのライブラリ「uuid58」を作成しました。

https://github.com/nakanoasaservice/uuid58

追記: また、これを簡単に試すためのウェブサイトも作成しました!

https://nakanoasaservice.github.io/uuid58-playground/

本記事ではuuid58を作った背景や使い方、特徴を紹介します。

動機

データベースの主キーにUUIDを使うことは多いですが、UUIDは36文字(ハイフン込み)と長く、URLに埋め込むと見た目も悪くなりがちです。
「短くしたい!」と思ってBase64エンコードを考える人も多いですが、Base64は/+=などURLで使いにくい文字が含まれてしまいます。
そこで、Bitcoinでも使われているBase58(視認性の悪い文字を除外したアルファベット)でエンコードすれば、

  • 22文字に短縮できる
  • URLセーフ
  • 見た目もスッキリ
    • 0OIlのような区別しづらい文字が除外されている
    • ダブルクリックで選択可能

という理想的なIDが作れます。

既存のUUID→Base58変換ライブラリを探してみたのですが、

  • Node.jsのcryptoに依存していてWeb標準で動かない
  • パフォーマンスに無駄がある
  • エラー処理が甘い(不正な文字列でも不完全にエンコード・デコードできてしまう)

など、なかなか「これだ!」というものが見つかりませんでした。
「じゃあ自分で作ろう!」と思い立ち、このuuid58ライブラリを開発しました。

使い方

uuid58は、UUIDのエンコード・デコード・生成を簡単に行うことができます。

import { uuid58, uuid58Decode, uuid58Encode } from "@nakanoaas/uuid58";

// 既存のUUIDをBase58に変換
const encoded = uuid58Encode("f4b247fd-1f87-45d4-aa06-1c6fc0a8dfaf");
// => "XDY9dmBbcMBXqcRvYw8xJ2"

// Base58からUUIDにデコード
const decoded = uuid58Decode("XDY9dmBbcMBXqcRvYw8xJ2");
// => "f4b247fd-1f87-45d4-aa06-1c6fc0a8dfaf"

// 新しいBase58エンコードUUIDを生成
const id = uuid58();
// 例: "XDY9dmBbcMBXqcRvYw8xJ2"

また、エラーを返すSafe系APIも用意されています。

import {
  Uuid58DecodeError,
  uuid58DecodeSafe,
  Uuid58EncodeError,
  uuid58EncodeSafe,
} from "@nakanoaas/uuid58";

const encodedSafe = uuid58EncodeSafe("invalid-uuid");
if (encodedSafe instanceof Uuid58EncodeError) {
  // エラー処理
  return;
}
// 以降encodedSafeはstring

const decodedSafe = uuid58DecodeSafe("invalid-base58");
if (decodedSafe instanceof Uuid58DecodeError) {
  // エラー処理
  return;
}
// 以降decodedSafeはstring

データベース保存時の注意

uuid58でエンコードしたIDはURLや外部公開用には非常に便利ですが、データベースに保存する際は注意が必要です
多くのRDBではUUID型カラムがサポートされており、その場合はBase58文字列ではなく、UUID型として保存することを推奨します
Base58文字列で保存すると、22文字のテキスト(UTF-8では22バイト)となり、16バイトのバイナリで保存されるUUID型よりも無駄にストレージを消費し、検索性能なども劣化します。

  • 外部公開やURL用:Base58文字列
  • DB保存:UUID型(バイナリ/UUIDカラム)

必要に応じてアプリケーション側で相互変換してください。

uuid58の特徴

  • Web標準のcrypto.randomUUID()に依存
    Deno/Node.js/ブラウザなど、どこでも動きます。

  • 不正な入力にはしっかりエラーを返す
    例外を投げるAPI(uuid58Encode/uuid58Decode)に加え、
    エラーオブジェクトを返すSafe系API(uuid58EncodeSafe/uuid58DecodeSafe)も用意しています。

  • Result型っぽいAPIも用意
    例外を投げるAPIだと

    let encoded;
    try {
      encoded = uuid58Encode(...);
    } catch (e) {
      // エラー処理
    }
    

    のようにletで変数を宣言しないといけませんが、
    Safe系APIなら

    const encoded = uuid58EncodeSafe(...);
    if (encoded instanceof Uuid58EncodeError) {
      // エラー処理
      return;
    }
    // encodedはstring型
    

    のように、constでスッキリ書けます。

  • 細かいパフォーマンスチューニング
    変換処理をできるだけ高速に、かつ安全に実装しています。

  • ファイル分割でtree-shakingしやすい
    encode/decode/uuid生成など用途ごとにファイルを分割し、必要な機能だけimportできます。

  • Denoで実装し、JSRにpublish
    Denoのエコシステムに乗せつつ、
    dntでnpm向けデュアルパッケージもビルドしてnpmにもpublishしています。

  • AIでTSDocやREADMEを英語で自動生成
    ドキュメント整備もAIの力で効率化しました。

感想

AIの力を借りて、思ったよりも簡単に実装・ドキュメント整備までできました。
UUIDを短く・URLフレンドリーにしたい方、ぜひuuid58を使ってみてください!

質問・要望などあればIssueやX(@nakanoaas)までお気軽にどうぞ!

Base58 vs Base64URL

「UUIDを短くするなら、Base64URLの方が良いのでは?」という意見もよく聞かれます。確かにBase64URLも+/-_に置き換えることでURLセーフになり、有力な選択肢です。

両者を比較検討した結果、以下の理由からBase58を選択しました:

  1. 最大文字数の比較:
    128ビット(16バイト)のUUIDをエンコードする場合、どちらも最大22文字になります。

    • Base64URL: 16バイト × 8ビット = 128ビット 128ビット ÷ 6ビット/文字 =
      21.33... → 22文字(パディング込み)

    • Base58: log₅₈(2¹²⁸) = 21.850... → 22文字

  2. パフォーマンス:
    実際に両方実装して比較したところ、エンコード・デコード速度に大きな差はありませんでした。

  3. 視認性:
    Base58は数字の0、大文字のO、大文字のI、小文字のlなど、視認性の低い文字を除外しているため、人間が識別しやすいという利点があります。

バージョン1.0.0での仕様変更について

重要なお知らせ:v1.0.0でエンコード形式が「最大22文字の可変長」から「22文字固定長」に変更されました。

バージョン1.0.0より前のuuid58では、エンコード結果が最大22文字の可変長となっていましたが、
1.0.0以降は常に22文字の固定長となるよう仕様を変更しています。

これは「パディングの長さが違うエンコード文字列も同じUUIDとしてデコードできてしまい、1対1対応が崩れる」ことを回避するためです。

可変長にすることで削減できるデータサイズは0.1%程度とごくわずかであり、それよりも「UUID⇔Base58の1対1対応」「常に22文字で扱いやすい」ことを重視しました。詳しい削減率の比較は以下のスクラップを参照してください。

https://zenn.dev/naas/scraps/0b48b953c39a99

旧バージョンをお使いの方への注意

v1.0.0より前のバージョンでエンコードしたIDが22文字より短い場合、エンコードできません。

既にご活用されている方はお手数ですが移行をお願いします🙏

YOSHINANI

Discussion

LaPhLaPh

NanoIDと比較したときのメリットが気になるのですが、どうなのでしょうか?

Nakano as a ServiceNakano as a Service

NanoIDは新しくIDを生成することはできますが、既に存在するUUIDを短い文字列に変換する・元のUUIDに戻す機能はありません。
なので既にDBにUUIDのレコードが存在する・DBのIDはUUIDでないといけないと要件で決まっている時に使えるというメリットがあります。

またDBに保存する時を考えると、UUID型がサポートされているDBはUUIDをバイナリで保存するため、1文字を8bitで保存する文字列よりもサイズを小さくできます。サイズを比較すると次のような感じです。

  • UUIDの文字列: 36文字 x 8bit = 288bit
  • UUIDのバイナリ形式: 128bit
  • UUIDをuuid58でエンコードした文字列: 22文字 x 8bit = 176bit
  • NanoIDのデフォルト設定で生成した文字列: 21文字 x 8bit = 168bit

なのでUUIDをバイナリで保存できるならそれがDBにとって一番良く、その上でDBから取得したUUIDをuuid58で変換してURLなどに表示すればサイズと見た目の両方のメリットを享受できます。
一方で文字列で保存するしか無いならNanoIDがわずかに(5%ほど)良いです。